diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index ea48a19874..05b2d762ae 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -28,6 +28,8 @@ extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); +extern void SetAutoGenerateDefaultAppMenuItems (bool enabled); +extern bool GetAutoGenerateDefaultAppMenuItems (); extern void InitializeAvnApp(); extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 72f5aa0a7d..17746e1d1d 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -2,6 +2,7 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" +static bool s_generateDefaultAppMenuItems = true; static NSString* s_appTitle = @"Avalonia"; // Copyright (c) 2011 The Chromium Authors. All rights reserved. @@ -122,6 +123,12 @@ public: ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; return S_OK; } + + virtual HRESULT SetDisableDefaultApplicationMenuItems (bool enabled) override + { + SetAutoGenerateDefaultAppMenuItems(!enabled); + return S_OK; + } }; /// See "Using POSIX Threads in a Cocoa Application" section here: @@ -310,3 +317,13 @@ CGFloat PrimaryDisplayHeight() { return NSMaxY([[[NSScreen screens] firstObject] frame]); } + +void SetAutoGenerateDefaultAppMenuItems (bool enabled) +{ + s_generateDefaultAppMenuItems = enabled; +} + +bool GetAutoGenerateDefaultAppMenuItems () +{ + return s_generateDefaultAppMenuItems; +} diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 198b01714f..ea5cca9ce8 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -445,47 +445,50 @@ extern void SetAppMenu (NSString* appName, IAvnMenu* menu) auto appMenu = [s_appMenuItem submenu]; - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Services item and menu - auto servicesItem = [[NSMenuItem alloc] init]; - servicesItem.title = @"Services"; - NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; - servicesItem.submenu = servicesMenu; - [NSApplication sharedApplication].servicesMenu = servicesMenu; - [appMenu addItem:servicesItem]; - - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Hide Application - auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"]; - - [appMenu addItem:hideItem]; - - // Hide Others - auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" - action:@selector(hideOtherApplications:) - keyEquivalent:@"h"]; - - hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; - [appMenu addItem:hideAllOthersItem]; - - // Show All - auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" - action:@selector(unhideAllApplications:) - keyEquivalent:@""]; - - [appMenu addItem:showAllItem]; - - [appMenu addItem:[NSMenuItem separatorItem]]; - - // Quit Application - auto quitItem = [[NSMenuItem alloc] init]; - quitItem.title = [@"Quit " stringByAppendingString:appName]; - quitItem.keyEquivalent = @"q"; - quitItem.target = [AvnWindow class]; - quitItem.action = @selector(closeAll); - [appMenu addItem:quitItem]; + if(GetAutoGenerateDefaultAppMenuItems()) + { + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Services item and menu + auto servicesItem = [[NSMenuItem alloc] init]; + servicesItem.title = @"Services"; + NSMenu *servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; + servicesItem.submenu = servicesMenu; + [NSApplication sharedApplication].servicesMenu = servicesMenu; + [appMenu addItem:servicesItem]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Hide Application + auto hideItem = [[NSMenuItem alloc] initWithTitle:[@"Hide " stringByAppendingString:appName] action:@selector(hide:) keyEquivalent:@"h"]; + + [appMenu addItem:hideItem]; + + // Hide Others + auto hideAllOthersItem = [[NSMenuItem alloc] initWithTitle:@"Hide Others" + action:@selector(hideOtherApplications:) + keyEquivalent:@"h"]; + + hideAllOthersItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption; + [appMenu addItem:hideAllOthersItem]; + + // Show All + auto showAllItem = [[NSMenuItem alloc] initWithTitle:@"Show All" + action:@selector(unhideAllApplications:) + keyEquivalent:@""]; + + [appMenu addItem:showAllItem]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // Quit Application + auto quitItem = [[NSMenuItem alloc] init]; + quitItem.title = [@"Quit " stringByAppendingString:appName]; + quitItem.keyEquivalent = @"q"; + quitItem.target = [AvnWindow class]; + quitItem.action = @selector(closeAll); + [appMenu addItem:quitItem]; + } } else { diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index ca1d97290e..05142532e9 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -22,7 +22,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty DurationProperty = AvaloniaProperty.RegisterDirect( - nameof(_duration), + nameof(Duration), o => o._duration, (o, v) => o._duration = v); @@ -31,7 +31,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty IterationCountProperty = AvaloniaProperty.RegisterDirect( - nameof(_iterationCount), + nameof(IterationCount), o => o._iterationCount, (o, v) => o._iterationCount = v); @@ -40,7 +40,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty PlaybackDirectionProperty = AvaloniaProperty.RegisterDirect( - nameof(_playbackDirection), + nameof(PlaybackDirection), o => o._playbackDirection, (o, v) => o._playbackDirection = v); @@ -49,7 +49,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty FillModeProperty = AvaloniaProperty.RegisterDirect( - nameof(_fillMode), + nameof(FillMode), o => o._fillMode, (o, v) => o._fillMode = v); @@ -58,7 +58,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty EasingProperty = AvaloniaProperty.RegisterDirect( - nameof(_easing), + nameof(Easing), o => o._easing, (o, v) => o._easing = v); @@ -67,7 +67,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty DelayProperty = AvaloniaProperty.RegisterDirect( - nameof(_delay), + nameof(Delay), o => o._delay, (o, v) => o._delay = v); @@ -76,7 +76,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty DelayBetweenIterationsProperty = AvaloniaProperty.RegisterDirect( - nameof(_delayBetweenIterations), + nameof(DelayBetweenIterations), o => o._delayBetweenIterations, (o, v) => o._delayBetweenIterations = v); @@ -85,7 +85,7 @@ namespace Avalonia.Animation /// public static readonly DirectProperty SpeedRatioProperty = AvaloniaProperty.RegisterDirect( - nameof(_speedRatio), + nameof(SpeedRatio), o => o._speedRatio, (o, v) => o._speedRatio = v, defaultBindingMode: BindingMode.TwoWay); diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 6740c2168f..9329cdd6af 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -12,5 +12,11 @@ namespace Avalonia.Data.Converters /// public static readonly IMultiValueConverter And = new FuncMultiValueConverter(x => x.All(y => y)); + + /// + /// A multi-value converter that returns true if any of the inputs is true. + /// + public static readonly IMultiValueConverter Or = + new FuncMultiValueConverter(x => x.Any(y => y)); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index e513a7b678..7e95dd100c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.Utilities; namespace Avalonia.Controls @@ -22,6 +23,7 @@ namespace Avalonia.Controls o => o.CellTemplate, (o, v) => o.CellTemplate = v); + [Content] public IDataTemplate CellTemplate { get { return _cellTemplate; } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 20ca41bc57..c5af5ffa7a 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -76,6 +77,14 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsTextSearchEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + + private string _textSearchTerm = string.Empty; + private DispatcherTimer _textSearchTimer; private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -164,6 +173,15 @@ namespace Avalonia.Controls set { SetValue(VerticalContentAlignmentProperty, value); } } + /// + /// Gets or sets a value that specifies whether a user can jump to a value by typing. + /// + public bool IsTextSearchEnabled + { + get { return GetValue(IsTextSearchEnabledProperty); } + set { SetValue(IsTextSearchEnabledProperty, value); } + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -229,6 +247,32 @@ namespace Avalonia.Controls } } + /// + protected override void OnTextInput(TextInputEventArgs e) + { + if (!IsTextSearchEnabled || e.Handled) + return; + + StopTextSearchTimer(); + + _textSearchTerm += e.Text; + + bool match(ItemContainerInfo info) => + info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + + var info = ItemContainerGenerator.Containers.FirstOrDefault(match); + + if (info != null) + { + SelectedIndex = info.Index; + } + + StartTextSearchTimer(); + + e.Handled = true; + } + /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { @@ -426,5 +470,31 @@ namespace Avalonia.Controls SelectedIndex = prev; } + + private void StartTextSearchTimer() + { + _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _textSearchTimer.Tick += TextSearchTimer_Tick; + _textSearchTimer.Start(); + } + + private void StopTextSearchTimer() + { + if (_textSearchTimer == null) + { + return; + } + + _textSearchTimer.Stop(); + _textSearchTimer.Tick -= TextSearchTimer_Tick; + + _textSearchTimer = null; + } + + private void TextSearchTimer_Tick(object sender, EventArgs e) + { + _textSearchTerm = string.Empty; + StopTextSearchTimer(); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs new file mode 100644 index 0000000000..63ac3ab62f --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Diagnostics.Converters +{ + internal class BoolToOpacityConverter : IValueConverter + { + public double Opacity { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool)value ? 1d : Opacity; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index fa41eacbeb..32592559e5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -1,8 +1,15 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Reflection; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels @@ -12,6 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IVisual _control; private readonly IDictionary> _propertyIndex; private AvaloniaPropertyViewModel _selectedProperty; + private string _styleFilter; + private bool _snapshotStyles; + private bool _showInactiveStyles; + private string _styleStatus; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { @@ -43,20 +54,160 @@ namespace Avalonia.Diagnostics.ViewModels { ao.PropertyChanged += ControlPropertyChanged; } + + AppliedStyles = new ObservableCollection(); + PseudoClasses = new ObservableCollection(); + + if (control is StyledElement styledElement) + { + styledElement.Classes.CollectionChanged += OnClassesChanged; + + var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes(true); + + foreach (var classAttribute in pseudoClassAttributes) + { + foreach (var className in classAttribute.PseudoClasses) + { + PseudoClasses.Add(new PseudoClassViewModel(className, styledElement)); + } + } + + var styleDiagnostics = styledElement.GetStyleDiagnostics(); + + foreach (var appliedStyle in styleDiagnostics.AppliedStyles) + { + var styleSource = appliedStyle.Source; + + var setters = new List(); + + if (styleSource is Style style) + { + foreach (var setter in style.Setters) + { + if (setter is Setter regularSetter) + { + var setterValue = regularSetter.Value; + + var resourceInfo = GetResourceInfo(setterValue); + + SetterViewModel setterVm; + + if (resourceInfo.HasValue) + { + var resourceKey = resourceInfo.Value.resourceKey; + var resourceValue = styledElement.FindResource(resourceKey); + + setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic); + } + else + { + setterVm = new SetterViewModel(regularSetter.Property, setterValue); + } + + setters.Add(setterVm); + } + } + + AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters)); + } + } + + UpdateStyles(); + } + } + + private (object resourceKey, bool isDynamic)? GetResourceInfo(object value) + { + if (value is StaticResourceExtension staticResource) + { + return (staticResource.ResourceKey, false); + } + else if (value is DynamicResourceExtension dynamicResource) + { + return (dynamicResource.ResourceKey, true); + } + + return null; } public TreePageViewModel TreePage { get; } public DataGridCollectionView PropertiesView { get; } + public ObservableCollection AppliedStyles { get; } + + public ObservableCollection PseudoClasses { get; } + public AvaloniaPropertyViewModel SelectedProperty { get => _selectedProperty; set => RaiseAndSetIfChanged(ref _selectedProperty, value); } - + + public string StyleFilter + { + get => _styleFilter; + set => RaiseAndSetIfChanged(ref _styleFilter, value); + } + + public bool SnapshotStyles + { + get => _snapshotStyles; + set => RaiseAndSetIfChanged(ref _snapshotStyles, value); + } + + public bool ShowInactiveStyles + { + get => _showInactiveStyles; + set => RaiseAndSetIfChanged(ref _showInactiveStyles, value); + } + + public string StyleStatus + { + get => _styleStatus; + set => RaiseAndSetIfChanged(ref _styleStatus, value); + } + public ControlLayoutViewModel Layout { get; } + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.PropertyName == nameof(StyleFilter)) + { + UpdateStyleFilters(); + } + else if (e.PropertyName == nameof(SnapshotStyles)) + { + if (!SnapshotStyles) + { + UpdateStyles(); + } + } + } + + private void UpdateStyleFilters() + { + var filter = StyleFilter; + bool hasFilter = !string.IsNullOrEmpty(filter); + + foreach (var style in AppliedStyles) + { + var hasVisibleSetter = false; + + foreach (var setter in style.Setters) + { + setter.IsVisible = + !hasFilter || setter.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0; + + hasVisibleSetter |= setter.IsVisible; + } + + style.IsVisible = hasVisibleSetter; + } + } + public void Dispose() { if (_control is INotifyPropertyChanged inpc) @@ -68,6 +219,11 @@ namespace Avalonia.Diagnostics.ViewModels { ao.PropertyChanged -= ControlPropertyChanged; } + + if (_control is StyledElement se) + { + se.Classes.CollectionChanged -= OnClassesChanged; + } } private IEnumerable GetAvaloniaProperties(object o) @@ -129,6 +285,74 @@ namespace Avalonia.Diagnostics.ViewModels property.Update(); } } + + if (!SnapshotStyles) + { + UpdateStyles(); + } + } + + private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (!SnapshotStyles) + { + UpdateStyles(); + } + } + + private void UpdateStyles() + { + int activeCount = 0; + + foreach (var style in AppliedStyles) + { + style.Update(); + + if (style.IsActive) + { + activeCount++; + } + } + + var propertyBuckets = new Dictionary>(); + + foreach (var style in AppliedStyles) + { + if (!style.IsActive) + { + continue; + } + + foreach (var setter in style.Setters) + { + if (propertyBuckets.TryGetValue(setter.Property, out var setters)) + { + foreach (var otherSetter in setters) + { + otherSetter.IsActive = false; + } + + setter.IsActive = true; + + setters.Add(setter); + } + else + { + setter.IsActive = true; + + setters = new List { setter }; + + propertyBuckets.Add(setter.Property, setters); + } + } + } + + foreach (var pseudoClass in PseudoClasses) + { + pseudoClass.Update(); + } + + StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)"; } private bool FilterProperty(object arg) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index bf7d0e232a..3049431361 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -163,6 +163,14 @@ namespace Avalonia.Diagnostics.ViewModels tree?.SelectControl(control); } + public void EnableSnapshotStyles(bool enable) + { + if (Content is TreePageViewModel treeVm && treeVm.Details != null) + { + treeVm.Details.SnapshotStyles = enable; + } + } + public void Dispose() { KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs new file mode 100644 index 0000000000..69126c2e2f --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class PseudoClassViewModel : ViewModelBase + { + private readonly IPseudoClasses _pseudoClasses; + private readonly StyledElement _source; + private bool _isActive; + private bool _isUpdating; + + public PseudoClassViewModel(string name, StyledElement source) + { + Name = name; + _source = source; + _pseudoClasses = _source.Classes; + + Update(); + } + + public string Name { get; } + + public bool IsActive + { + get => _isActive; + set + { + RaiseAndSetIfChanged(ref _isActive, value); + + if (!_isUpdating) + { + _pseudoClasses.Set(Name, value); + } + } + } + + public void Update() + { + try + { + _isUpdating = true; + + IsActive = _source.Classes.Contains(Name); + } + finally + { + _isUpdating = false; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs new file mode 100644 index 0000000000..a82e13fcfa --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs @@ -0,0 +1,27 @@ +using Avalonia.Media; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class ResourceSetterViewModel : SetterViewModel + { + public object Key { get; } + + public IBrush Tint { get; } + + public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue) + { + Key = resourceKey; + Tint = isDynamic ? Brushes.Orange : Brushes.Brown; + } + + public void CopyResourceKey() + { + if (Key is null) + { + return; + } + + CopyToClipboard(Key.ToString()); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs new file mode 100644 index 0000000000..e835f5a878 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs @@ -0,0 +1,59 @@ +using Avalonia.Input.Platform; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class SetterViewModel : ViewModelBase + { + private bool _isActive; + private bool _isVisible; + + public AvaloniaProperty Property { get; } + + public string Name { get; } + + public object Value { get; } + + public bool IsActive + { + get => _isActive; + set => RaiseAndSetIfChanged(ref _isActive, value); + } + + public bool IsVisible + { + get => _isVisible; + set => RaiseAndSetIfChanged(ref _isVisible, value); + } + + public SetterViewModel(AvaloniaProperty property, object value) + { + Property = property; + Name = property.Name; + Value = value; + IsActive = true; + IsVisible = true; + } + + public void CopyValue() + { + if (Value is null) + { + return; + } + + CopyToClipboard(Value.ToString()); + } + + public void CopyPropertyName() + { + CopyToClipboard(Property.Name); + } + + protected static void CopyToClipboard(string value) + { + var clipboard = AvaloniaLocator.Current.GetService(); + + clipboard?.SetTextAsync(value); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs new file mode 100644 index 0000000000..06e2409800 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class StyleViewModel : ViewModelBase + { + private readonly IStyleInstance _styleInstance; + private bool _isActive; + private bool _isVisible; + + public StyleViewModel(IStyleInstance styleInstance, string name, List setters) + { + _styleInstance = styleInstance; + IsVisible = true; + Name = name; + Setters = setters; + + Update(); + } + + public bool IsActive + { + get => _isActive; + set => RaiseAndSetIfChanged(ref _isActive, value); + } + + public bool IsVisible + { + get => _isVisible; + set => RaiseAndSetIfChanged(ref _isVisible, value); + } + + public string Name { get; } + + public List Setters { get; } + + public void Update() + { + IsActive = _styleInstance.IsActive; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index bd65a3b06b..6b779cd6ac 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -31,11 +31,18 @@ namespace Avalonia.Diagnostics.ViewModels get => _selectedNode; private set { + var oldDetails = Details; + if (RaiseAndSetIfChanged(ref _selectedNode, value)) { Details = value != null ? new ControlDetailsViewModel(this, value.Visual) : null; + + if (Details != null && oldDetails != null) + { + Details.StyleFilter = oldDetails.StyleFilter; + } } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 2e0b6813ba..9ba576c826 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -2,7 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" - x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"> + xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" + x:Class="Avalonia.Diagnostics.Views.ControlDetailsView" + x:Name="Main"> @@ -11,6 +13,7 @@ + @@ -105,7 +108,7 @@ - + @@ -148,7 +151,122 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ( + + + ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index c4f9185728..330121321a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -90,6 +90,16 @@ namespace Avalonia.Diagnostics.Views var vm = (MainViewModel)DataContext; vm.SelectControl((IControl)control); } + } + else if (e.Modifiers == RawInputModifiers.Alt) + { + if (e.Key == Key.S || e.Key == Key.D) + { + var enable = e.Key == Key.S; + + var vm = (MainViewModel)DataContext; + vm.EnableSnapshotStyles(enable); + } } } diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 68f08d7cbb..6b147590ab 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -447,7 +447,7 @@ namespace Avalonia.Layout // and only use the layout when to clear it when it's done. gridState.EnsureFirstElementOwnership(context); - return new Size(desiredSize.Width, desiredSize.Height); + return desiredSize; } protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index 62c5174775..282bbab1a8 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -44,7 +44,7 @@ namespace Avalonia.Layout Size availableSize, VirtualizingLayoutContext context, double layoutItemWidth, - double LayoutItemHeight, + double layoutItemHeight, UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, @@ -63,7 +63,7 @@ namespace Avalonia.Layout if (realizedElement != null) { realizedElement.Measure(availableSize); - SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); + SetSize(realizedElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); _cachedFirstElement = null; } else @@ -78,7 +78,7 @@ namespace Avalonia.Layout _cachedFirstElement.Measure(availableSize); - SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); + SetSize(_cachedFirstElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); @@ -93,7 +93,7 @@ namespace Avalonia.Layout private void SetSize( ILayoutable element, double layoutItemWidth, - double LayoutItemHeight, + double layoutItemHeight, Size availableSize, UniformGridLayoutItemsStretch stretch, Orientation orientation, @@ -107,7 +107,7 @@ namespace Avalonia.Layout } EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); - EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); + EffectiveItemHeight = (double.IsNaN(layoutItemHeight) ? element.DesiredSize.Height : layoutItemHeight); var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height; var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index edcbf90ebc..35d21a75d3 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -92,6 +92,8 @@ namespace Avalonia.Native var macOpts = AvaloniaLocator.Current.GetService(); _factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0); + _factory.MacOptions.SetDisableDefaultApplicationMenuItems( + macOpts?.DisableDefaultApplicationMenuItems == true ? 1 : 0); } AvaloniaLocator.CurrentMutable diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 545659f813..76cb7a8057 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -38,5 +38,7 @@ namespace Avalonia public class MacOSPlatformOptions { public bool ShowInDock { get; set; } = true; + + public bool DisableDefaultApplicationMenuItems { get; set; } } } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 3627ff6894..57a0c32067 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -528,6 +528,7 @@ interface IAvnMacOptions : IUnknown { HRESULT SetShowInDock(int show); HRESULT SetApplicationTitle(char* utf8string); + HRESULT SetDisableDefaultApplicationMenuItems(bool enabled); } [uuid(04c1b049-1f43-418a-9159-cae627ec1367)] diff --git a/src/Avalonia.Styling/ApiCompatBaseline.txt b/src/Avalonia.Styling/ApiCompatBaseline.txt new file mode 100644 index 0000000000..0eedc3e360 --- /dev/null +++ b/src/Avalonia.Styling/ApiCompatBaseline.txt @@ -0,0 +1,4 @@ +Compat issues with assembly Avalonia.Styling: +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive.get()' is present in the implementation but not in the contract. +Total Issues: 2 diff --git a/src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs b/src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs new file mode 100644 index 0000000000..984b145e68 --- /dev/null +++ b/src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics +{ + /// + /// Contains information about style related diagnostics of a control. + /// + public class StyleDiagnostics + { + /// + /// Currently applied styles. + /// + public IReadOnlyList AppliedStyles { get; } + + public StyleDiagnostics(IReadOnlyList appliedStyles) + { + AppliedStyles = appliedStyles; + } + } +} diff --git a/src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs b/src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs new file mode 100644 index 0000000000..d7bcc1aa47 --- /dev/null +++ b/src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs @@ -0,0 +1,17 @@ +namespace Avalonia.Diagnostics +{ + /// + /// Defines diagnostic extensions on s. + /// + public static class StyledElementExtensions + { + /// + /// Gets a style diagnostics for a . + /// + /// The element. + public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement) + { + return styledElement.GetStyleDiagnosticsInternal(); + } + } +} diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 9beb3b4240..5f26f65512 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -365,6 +365,18 @@ namespace Avalonia } } + internal StyleDiagnostics GetStyleDiagnosticsInternal() + { + IReadOnlyList? appliedStyles = _appliedStyles; + + if (appliedStyles is null) + { + appliedStyles = Array.Empty(); + } + + return new StyleDiagnostics(appliedStyles); + } + /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Styling/Styling/IStyleInstance.cs index cb094badd2..8ddb989bc0 100644 --- a/src/Avalonia.Styling/Styling/IStyleInstance.cs +++ b/src/Avalonia.Styling/Styling/IStyleInstance.cs @@ -14,6 +14,11 @@ namespace Avalonia.Styling /// IStyle Source { get; } + /// + /// Gets a value indicating whether this style is active. + /// + bool IsActive { get; } + /// /// Instructs the style to start acting upon the control. /// diff --git a/src/Avalonia.Styling/Styling/StyleInstance.cs b/src/Avalonia.Styling/Styling/StyleInstance.cs index 8ca31d654f..830cf49a0d 100644 --- a/src/Avalonia.Styling/Styling/StyleInstance.cs +++ b/src/Avalonia.Styling/Styling/StyleInstance.cs @@ -17,7 +17,6 @@ namespace Avalonia.Styling private readonly List? _animations; private readonly IStyleActivator? _activator; private readonly Subject? _animationTrigger; - private bool _active; public StyleInstance( IStyle source, @@ -29,6 +28,7 @@ namespace Avalonia.Styling Source = source ?? throw new ArgumentNullException(nameof(source)); Target = target ?? throw new ArgumentNullException(nameof(target)); _activator = activator; + IsActive = _activator is null; if (setters is object) { @@ -56,6 +56,7 @@ namespace Avalonia.Styling } } + public bool IsActive { get; private set; } public IStyle Source { get; } public IStyleable Target { get; } @@ -104,15 +105,15 @@ namespace Avalonia.Styling private void ActivatorChanged(bool value) { - if (_active != value) + if (IsActive != value) { - _active = value; + IsActive = value; _animationTrigger?.OnNext(value); if (_setters is object) { - if (_active) + if (IsActive) { foreach (var setter in _setters) { diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 9ce19cff6e..4bfae4c223 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -59,8 +59,7 @@ Grid.Column="4"/> new ComboBoxItem { Content = x }) + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedIndex = initialSelectedIndex; + + var args = new TextInputEventArgs + { + Text = searchTerm, + RoutedEvent = InputElement.TextInputEvent + }; + + target.RaiseEvent(args); + + Assert.Equal(expectedSelectedIndex, target.SelectedIndex); + } + } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs index 7c48a975ef..e5e63b24d0 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs @@ -117,9 +117,11 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = run(); result.Item1.Subscribe(x => { }); - GC.Collect(); + // Mono trickery + GC.Collect(2); GC.WaitForPendingFinalizers(); - GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(2); Assert.Null(result.Item2.Target); }