From e4cf7a6bf75c087769e738bb14047072a2c28a53 Mon Sep 17 00:00:00 2001 From: luthfiampas Date: Thu, 4 Feb 2021 01:52:45 +0700 Subject: [PATCH 01/22] add auto-select functionality to ComboBox control --- src/Avalonia.Controls/ComboBox.cs | 72 +++++++++++++++++++ .../ComboBoxTests.cs | 37 ++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 7f2acb58fe..95baf8146d 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 IsAutoSelectEnabledProperty = + AvaloniaProperty.Register(nameof(IsAutoSelectEnabled)); + + private string _autoSelectTerm = string.Empty; + private DispatcherTimer _autoSelectTimer; private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -164,6 +173,17 @@ namespace Avalonia.Controls set { SetValue(VerticalContentAlignmentProperty, value); } } + /// + /// Gets or sets value indicating whether auto-select is currently enabled. + /// If true, the control will try to find then select matched + /// based on current keyboard inputs. + /// + public bool IsAutoSelectEnabled + { + get { return GetValue(IsAutoSelectEnabledProperty); } + set { SetValue(IsAutoSelectEnabledProperty, value); } + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -229,6 +249,32 @@ namespace Avalonia.Controls } } + /// + protected override void OnTextInput(TextInputEventArgs e) + { + if (!IsAutoSelectEnabled || e.Handled) + return; + + StopAutoSelectTimer(); + + _autoSelectTerm += e.Text; + + bool match(ItemContainerInfo info) => + info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_autoSelectTerm, StringComparison.OrdinalIgnoreCase) == true; + + var info = ItemContainerGenerator.Containers.FirstOrDefault(match); + + if (info != null) + { + SelectedIndex = info.Index; + } + + StartAutoSelectTimer(); + + e.Handled = true; + } + /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { @@ -426,5 +472,31 @@ namespace Avalonia.Controls SelectedIndex = prev; } + + private void StartAutoSelectTimer() + { + _autoSelectTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _autoSelectTimer.Tick += AutoSelectTimer_Tick; + _autoSelectTimer.Start(); + } + + private void StopAutoSelectTimer() + { + if (_autoSelectTimer == null) + { + return; + } + + _autoSelectTimer.Stop(); + _autoSelectTimer.Tick -= AutoSelectTimer_Tick; + + _autoSelectTimer = null; + } + + private void AutoSelectTimer_Tick(object sender, EventArgs e) + { + _autoSelectTerm = string.Empty; + StopAutoSelectTimer(); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index c8a30a42e9..218832d988 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -1,7 +1,9 @@ +using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.UnitTests; @@ -137,5 +139,40 @@ namespace Avalonia.Controls.UnitTests Assert.True(other.IsFocused); } } + + [Theory] + [InlineData(-1, 2, "c", "A item", "B item", "C item")] + [InlineData(0, 1, "b", "A item", "B item", "C item")] + [InlineData(2, 2, "x", "A item", "B item", "C item")] + public void AutoSelect_Should_Have_Expected_SelectedIndex( + int initialSelectedIndex, + int expectedSelectedIndex, + string searchTerm, + params string[] items) + { + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new ComboBox + { + IsAutoSelectEnabled = true, + Template = GetTemplate(), + Items = items.Select(x => 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); + } + } } } From e6952dc352a1de9476286a1170ff12462442eee8 Mon Sep 17 00:00:00 2001 From: luthfiampas Date: Wed, 24 Feb 2021 14:46:40 +0700 Subject: [PATCH 02/22] rename AutoSelect to TextSearch --- src/Avalonia.Controls/ComboBox.cs | 54 +++++++++---------- .../ComboBoxTests.cs | 4 +- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 95baf8146d..fa467a83ac 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -78,13 +78,13 @@ namespace Avalonia.Controls ContentControl.VerticalContentAlignmentProperty.AddOwner(); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty IsAutoSelectEnabledProperty = - AvaloniaProperty.Register(nameof(IsAutoSelectEnabled)); + public static readonly StyledProperty IsTextSearchEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSearchEnabled)); - private string _autoSelectTerm = string.Empty; - private DispatcherTimer _autoSelectTimer; + private string _textSearchTerm = string.Empty; + private DispatcherTimer _textSearchTimer; private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -174,14 +174,12 @@ namespace Avalonia.Controls } /// - /// Gets or sets value indicating whether auto-select is currently enabled. - /// If true, the control will try to find then select matched - /// based on current keyboard inputs. + /// Gets or sets a value that specifies whether a user can jump to a value by typing. /// - public bool IsAutoSelectEnabled + public bool IsTextSearchEnabled { - get { return GetValue(IsAutoSelectEnabledProperty); } - set { SetValue(IsAutoSelectEnabledProperty, value); } + get { return GetValue(IsTextSearchEnabledProperty); } + set { SetValue(IsTextSearchEnabledProperty, value); } } /// @@ -252,16 +250,16 @@ namespace Avalonia.Controls /// protected override void OnTextInput(TextInputEventArgs e) { - if (!IsAutoSelectEnabled || e.Handled) + if (!IsTextSearchEnabled || e.Handled) return; - StopAutoSelectTimer(); + StopTextSearchTimer(); - _autoSelectTerm += e.Text; + _textSearchTerm += e.Text; bool match(ItemContainerInfo info) => info.ContainerControl is IContentControl control && - control.Content?.ToString()?.StartsWith(_autoSelectTerm, StringComparison.OrdinalIgnoreCase) == true; + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; var info = ItemContainerGenerator.Containers.FirstOrDefault(match); @@ -270,7 +268,7 @@ namespace Avalonia.Controls SelectedIndex = info.Index; } - StartAutoSelectTimer(); + StartTextSearchTimer(); e.Handled = true; } @@ -473,30 +471,30 @@ namespace Avalonia.Controls SelectedIndex = prev; } - private void StartAutoSelectTimer() + private void StartTextSearchTimer() { - _autoSelectTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; - _autoSelectTimer.Tick += AutoSelectTimer_Tick; - _autoSelectTimer.Start(); + _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _textSearchTimer.Tick += TextSearchTimer_Tick; + _textSearchTimer.Start(); } - private void StopAutoSelectTimer() + private void StopTextSearchTimer() { - if (_autoSelectTimer == null) + if (_textSearchTimer == null) { return; } - _autoSelectTimer.Stop(); - _autoSelectTimer.Tick -= AutoSelectTimer_Tick; + _textSearchTimer.Stop(); + _textSearchTimer.Tick -= TextSearchTimer_Tick; - _autoSelectTimer = null; + _textSearchTimer = null; } - private void AutoSelectTimer_Tick(object sender, EventArgs e) + private void TextSearchTimer_Tick(object sender, EventArgs e) { - _autoSelectTerm = string.Empty; - StopAutoSelectTimer(); + _textSearchTerm = string.Empty; + StopTextSearchTimer(); } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 218832d988..7cd7e74976 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -144,7 +144,7 @@ namespace Avalonia.Controls.UnitTests [InlineData(-1, 2, "c", "A item", "B item", "C item")] [InlineData(0, 1, "b", "A item", "B item", "C item")] [InlineData(2, 2, "x", "A item", "B item", "C item")] - public void AutoSelect_Should_Have_Expected_SelectedIndex( + public void TextSearch_Should_Have_Expected_SelectedIndex( int initialSelectedIndex, int expectedSelectedIndex, string searchTerm, @@ -154,7 +154,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ComboBox { - IsAutoSelectEnabled = true, + IsTextSearchEnabled = true, Template = GetTemplate(), Items = items.Select(x => new ComboBoxItem { Content = x }) }; From a4477e7de62870fe6082c4e306cd1d953c36b4e5 Mon Sep 17 00:00:00 2001 From: luthfiampas Date: Wed, 24 Feb 2021 15:08:30 +0700 Subject: [PATCH 03/22] set default value of IsTextSearchEnabled to true --- src/Avalonia.Controls/ComboBox.cs | 2 +- tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index fa467a83ac..8e7f01cad9 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -81,7 +81,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled)); + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); private string _textSearchTerm = string.Empty; private DispatcherTimer _textSearchTimer; diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 7cd7e74976..4ea838358c 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -154,7 +154,6 @@ namespace Avalonia.Controls.UnitTests { var target = new ComboBox { - IsTextSearchEnabled = true, Template = GetTemplate(), Items = items.Select(x => new ComboBoxItem { Content = x }) }; From 188faa07c5333b14e9c734b1c8a3cefa47da94b6 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 22 Feb 2021 09:47:12 +0100 Subject: [PATCH 04/22] WIP setup for inspecting styles. --- .../ViewModels/ControlDetailsViewModel.cs | 208 ++++++++++++++++++ .../Diagnostics/Views/ControlDetailsView.xaml | 49 ++++- src/Avalonia.Styling/StyledElement.cs | 30 +++ .../Styling/IStyleInstance.cs | 2 + src/Avalonia.Styling/Styling/StyleInstance.cs | 9 +- 5 files changed, 292 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index fa41eacbeb..6c622ad6c3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -1,12 +1,131 @@ 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.Media; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { + internal class StyleViewModel : ViewModelBase + { + private readonly IStyleInstance _styleInstance; + private bool _isActive; + + public StyleViewModel(IStyleInstance styleInstance, string name, List setters) + { + _styleInstance = styleInstance; + IsActive = styleInstance.IsActive; + Name = name; + Setters = setters; + } + + public bool IsActive + { + get => _isActive; + set => RaiseAndSetIfChanged(ref _isActive, value); + } + + public string Name { get; } + + public List Setters { get; } + + public void Update() + { + IsActive = _styleInstance.IsActive; + } + } + + internal enum ValueKind + { + Regular, + Resource + } + + internal class SetterViewModel : ViewModelBase + { + public string Name { get; } + + public object Value { get; } + + public ValueKind Kind { get; } + + public bool IsSpecialKind => Kind != ValueKind.Regular; + + public IBrush KindColor { get; } + + public SetterViewModel(string name, object value, ValueKind kind) + { + Name = name; + Value = value; + Kind = kind; + + if (Kind == ValueKind.Resource) + { + KindColor = Brushes.Brown; + } + else + { + KindColor = Brushes.Transparent; + } + } + } + + internal class PseudoClassesViewModel : ViewModelBase + { + private readonly StyledElement _source; + private readonly IPseudoClasses _pseudoClasses; + private bool _isActive; + private bool _isUpdating; + + public PseudoClassesViewModel(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; + } + } + } + internal class ControlDetailsViewModel : ViewModelBase, IDisposable { private readonly IVisual _control; @@ -43,12 +162,76 @@ 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 PseudoClassesViewModel(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; + + ValueKind kind = ValueKind.Regular; + + if (setterValue is DynamicResourceExtension dynResource) + { + var resolved = styledElement.FindResource(dynResource.ResourceKey); + + if (resolved != null) + { + setterValue = $"{resolved} ({dynResource.ResourceKey})"; + } + else + { + setterValue = dynResource.ResourceKey; + } + + kind = ValueKind.Resource; + } + + setters.Add(new SetterViewModel(regularSetter.Property?.Name ?? "?", setterValue, kind)); + } + } + + AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters)); + } + } + } } public TreePageViewModel TreePage { get; } public DataGridCollectionView PropertiesView { get; } + public ObservableCollection AppliedStyles { get; } + + public ObservableCollection PseudoClasses { get; } + public AvaloniaPropertyViewModel SelectedProperty { get => _selectedProperty; @@ -68,6 +251,11 @@ namespace Avalonia.Diagnostics.ViewModels { ao.PropertyChanged -= ControlPropertyChanged; } + + if (_control is StyledElement se) + { + se.Classes.CollectionChanged -= OnClassesChanged; + } } private IEnumerable GetAvaloniaProperties(object o) @@ -129,6 +317,26 @@ namespace Avalonia.Diagnostics.ViewModels property.Update(); } } + + UpdateStyles(); + } + + private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateStyles(); + } + + private void UpdateStyles() + { + foreach (var style in AppliedStyles) + { + style.Update(); + } + + foreach (var pseudoClass in PseudoClasses) + { + pseudoClass.Update(); + } } private bool FilterProperty(object arg) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 2e0b6813ba..9b341c1388 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -105,7 +105,7 @@ - + @@ -148,7 +148,52 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index cc8d91462d..2f7ec13775 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -16,6 +16,24 @@ using Avalonia.Styling; namespace Avalonia { + public class StyleDiagnostics + { + public IReadOnlyList AppliedStyles { get; } + + public StyleDiagnostics(IReadOnlyList appliedStyles) + { + AppliedStyles = appliedStyles; + } + } + + public static class StyledElementExtensions + { + public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement) + { + return styledElement.GetStyleDiagnosticsInternal(); + } + } + /// /// Extends an with the following features: /// @@ -356,6 +374,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..cdccc552a3 100644 --- a/src/Avalonia.Styling/Styling/IStyleInstance.cs +++ b/src/Avalonia.Styling/Styling/IStyleInstance.cs @@ -14,6 +14,8 @@ namespace Avalonia.Styling /// IStyle Source { get; } + 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) { From e428d5601aec3ff77ac7049be59729f214e1c8b1 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 22 Feb 2021 19:14:36 +0100 Subject: [PATCH 05/22] General UX improvements. --- .../Converters/BoolToOpacityConverter.cs | 21 ++ .../ViewModels/ControlDetailsViewModel.cs | 227 ++++++++---------- .../ViewModels/PseudoClassesViewModel.cs | 51 ++++ .../ViewModels/ResourceSetterViewModel.cs | 17 ++ .../Diagnostics/ViewModels/SetterViewModel.cs | 35 +++ .../Diagnostics/ViewModels/StyleViewModel.cs | 43 ++++ .../ViewModels/TreePageViewModel.cs | 7 + .../Diagnostics/Views/ControlDetailsView.xaml | 70 +++++- 8 files changed, 334 insertions(+), 137 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassesViewModel.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs 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 6c622ad6c3..e6758d680c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -9,128 +9,17 @@ using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Markup.Xaml.MarkupExtensions; -using Avalonia.Media; using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class StyleViewModel : ViewModelBase - { - private readonly IStyleInstance _styleInstance; - private bool _isActive; - - public StyleViewModel(IStyleInstance styleInstance, string name, List setters) - { - _styleInstance = styleInstance; - IsActive = styleInstance.IsActive; - Name = name; - Setters = setters; - } - - public bool IsActive - { - get => _isActive; - set => RaiseAndSetIfChanged(ref _isActive, value); - } - - public string Name { get; } - - public List Setters { get; } - - public void Update() - { - IsActive = _styleInstance.IsActive; - } - } - - internal enum ValueKind - { - Regular, - Resource - } - - internal class SetterViewModel : ViewModelBase - { - public string Name { get; } - - public object Value { get; } - - public ValueKind Kind { get; } - - public bool IsSpecialKind => Kind != ValueKind.Regular; - - public IBrush KindColor { get; } - - public SetterViewModel(string name, object value, ValueKind kind) - { - Name = name; - Value = value; - Kind = kind; - - if (Kind == ValueKind.Resource) - { - KindColor = Brushes.Brown; - } - else - { - KindColor = Brushes.Transparent; - } - } - } - - internal class PseudoClassesViewModel : ViewModelBase - { - private readonly StyledElement _source; - private readonly IPseudoClasses _pseudoClasses; - private bool _isActive; - private bool _isUpdating; - - public PseudoClassesViewModel(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; - } - } - } - internal class ControlDetailsViewModel : ViewModelBase, IDisposable { private readonly IVisual _control; private readonly IDictionary> _propertyIndex; private AvaloniaPropertyViewModel _selectedProperty; + private string _styleFilter; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { @@ -196,32 +85,46 @@ namespace Avalonia.Diagnostics.ViewModels { var setterValue = regularSetter.Value; - ValueKind kind = ValueKind.Regular; + var resourceInfo = GetResourceInfo(setterValue); + + SetterViewModel setterVm; - if (setterValue is DynamicResourceExtension dynResource) + if (resourceInfo.HasValue) { - var resolved = styledElement.FindResource(dynResource.ResourceKey); - - if (resolved != null) - { - setterValue = $"{resolved} ({dynResource.ResourceKey})"; - } - else - { - setterValue = dynResource.ResourceKey; - } - - kind = ValueKind.Resource; + 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(new SetterViewModel(regularSetter.Property?.Name ?? "?", setterValue, kind)); + 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; } @@ -237,9 +140,46 @@ namespace Avalonia.Diagnostics.ViewModels get => _selectedProperty; set => RaiseAndSetIfChanged(ref _selectedProperty, value); } + + public string StyleFilter + { + get => _styleFilter; + set => RaiseAndSetIfChanged(ref _styleFilter, value); + } public ControlLayoutViewModel Layout { get; } + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.PropertyName == nameof(StyleFilter)) + { + UpdateStyleFilters(); + } + } + + 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) @@ -333,6 +273,39 @@ namespace Avalonia.Diagnostics.ViewModels style.Update(); } + 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(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassesViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassesViewModel.cs new file mode 100644 index 0000000000..dd2452333e --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassesViewModel.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class PseudoClassesViewModel : ViewModelBase + { + private readonly IPseudoClasses _pseudoClasses; + private readonly StyledElement _source; + private bool _isActive; + private bool _isUpdating; + + public PseudoClassesViewModel(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..fedfa1c0b4 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs @@ -0,0 +1,17 @@ +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; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs new file mode 100644 index 0000000000..691817ccd1 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs @@ -0,0 +1,35 @@ +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; + } + } +} 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 9b341c1388..7b5fbbfb22 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" + xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"> @@ -11,6 +12,7 @@ + @@ -149,29 +151,77 @@ - + + + + - + - + + + + + + + + - - + + + + + + + + + + - - - - + + + + + + - + + + + + + + + ( + + + ) + + + + + + + + + + + + + + + + + + From 02fa3502e0438b8f04194a9fc51bfbdcdb1c2056 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 25 Feb 2021 23:20:07 +0100 Subject: [PATCH 06/22] Cleanup styling code. --- .../Diagnostics/StyleDiagnostics.cs | 21 +++++++++++++++++++ .../Diagnostics/StyledElementExtensions.cs | 17 +++++++++++++++ src/Avalonia.Styling/StyledElement.cs | 18 ---------------- .../Styling/IStyleInstance.cs | 3 +++ 4 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs create mode 100644 src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs 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 2f7ec13775..fad281244f 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -16,24 +16,6 @@ using Avalonia.Styling; namespace Avalonia { - public class StyleDiagnostics - { - public IReadOnlyList AppliedStyles { get; } - - public StyleDiagnostics(IReadOnlyList appliedStyles) - { - AppliedStyles = appliedStyles; - } - } - - public static class StyledElementExtensions - { - public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement) - { - return styledElement.GetStyleDiagnosticsInternal(); - } - } - /// /// Extends an with the following features: /// diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Styling/Styling/IStyleInstance.cs index cdccc552a3..8ddb989bc0 100644 --- a/src/Avalonia.Styling/Styling/IStyleInstance.cs +++ b/src/Avalonia.Styling/Styling/IStyleInstance.cs @@ -14,6 +14,9 @@ namespace Avalonia.Styling /// IStyle Source { get; } + /// + /// Gets a value indicating whether this style is active. + /// bool IsActive { get; } /// From bf40bae0ed0634a69db3ace9795a7e40e1fe2671 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 25 Feb 2021 23:23:14 +0100 Subject: [PATCH 07/22] API diff update. --- src/Avalonia.Styling/ApiCompatBaseline.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/Avalonia.Styling/ApiCompatBaseline.txt 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 From 161b9374a59a9631f2b2bfa976a0bf4eba21e457 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 26 Feb 2021 00:27:30 +0100 Subject: [PATCH 08/22] Add style snapshotting and context menus to copy property values. --- .../ViewModels/ControlDetailsViewModel.cs | 32 +++++++++++++++---- .../Diagnostics/ViewModels/MainViewModel.cs | 8 +++++ ...esViewModel.cs => PseudoClassViewModel.cs} | 4 +-- .../ViewModels/ResourceSetterViewModel.cs | 10 ++++++ .../Diagnostics/ViewModels/SetterViewModel.cs | 26 ++++++++++++++- .../Diagnostics/Views/ControlDetailsView.xaml | 22 +++++++++++-- .../Diagnostics/Views/MainWindow.xaml.cs | 10 ++++++ 7 files changed, 101 insertions(+), 11 deletions(-) rename src/Avalonia.Diagnostics/Diagnostics/ViewModels/{PseudoClassesViewModel.cs => PseudoClassViewModel.cs} (88%) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index e6758d680c..19eb14ba06 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -20,6 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IDictionary> _propertyIndex; private AvaloniaPropertyViewModel _selectedProperty; private string _styleFilter; + private bool _snapshotStyles; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { @@ -53,7 +54,7 @@ namespace Avalonia.Diagnostics.ViewModels } AppliedStyles = new ObservableCollection(); - PseudoClasses = new ObservableCollection(); + PseudoClasses = new ObservableCollection(); if (control is StyledElement styledElement) { @@ -65,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels { foreach (var className in classAttribute.PseudoClasses) { - PseudoClasses.Add(new PseudoClassesViewModel(className, styledElement)); + PseudoClasses.Add(new PseudoClassViewModel(className, styledElement)); } } @@ -133,7 +134,7 @@ namespace Avalonia.Diagnostics.ViewModels public ObservableCollection AppliedStyles { get; } - public ObservableCollection PseudoClasses { get; } + public ObservableCollection PseudoClasses { get; } public AvaloniaPropertyViewModel SelectedProperty { @@ -146,7 +147,13 @@ namespace Avalonia.Diagnostics.ViewModels get => _styleFilter; set => RaiseAndSetIfChanged(ref _styleFilter, value); } - + + public bool SnapshotStyles + { + get => _snapshotStyles; + set => RaiseAndSetIfChanged(ref _snapshotStyles, value); + } + public ControlLayoutViewModel Layout { get; } protected override void OnPropertyChanged(PropertyChangedEventArgs e) @@ -157,6 +164,13 @@ namespace Avalonia.Diagnostics.ViewModels { UpdateStyleFilters(); } + else if (e.PropertyName == nameof(SnapshotStyles)) + { + if (!SnapshotStyles) + { + UpdateStyles(); + } + } } private void UpdateStyleFilters() @@ -258,12 +272,18 @@ namespace Avalonia.Diagnostics.ViewModels } } - UpdateStyles(); + if (!SnapshotStyles) + { + UpdateStyles(); + } } private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e) { - UpdateStyles(); + if (!SnapshotStyles) + { + UpdateStyles(); + } } private void UpdateStyles() 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/PseudoClassesViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs similarity index 88% rename from src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassesViewModel.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs index dd2452333e..69126c2e2f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassesViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs @@ -2,14 +2,14 @@ namespace Avalonia.Diagnostics.ViewModels { - internal class PseudoClassesViewModel : ViewModelBase + internal class PseudoClassViewModel : ViewModelBase { private readonly IPseudoClasses _pseudoClasses; private readonly StyledElement _source; private bool _isActive; private bool _isUpdating; - public PseudoClassesViewModel(string name, StyledElement source) + public PseudoClassViewModel(string name, StyledElement source) { Name = name; _source = source; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs index fedfa1c0b4..a82e13fcfa 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs @@ -13,5 +13,15 @@ namespace Avalonia.Diagnostics.ViewModels 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 index 691817ccd1..e835f5a878 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Diagnostics.ViewModels +using Avalonia.Input.Platform; + +namespace Avalonia.Diagnostics.ViewModels { internal class SetterViewModel : ViewModelBase { @@ -31,5 +33,27 @@ 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/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 7b5fbbfb22..c7a52a5a5f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -152,8 +152,13 @@ - - + + + + + + + @@ -195,6 +200,13 @@ + + + + + + + @@ -210,6 +222,12 @@ + + + + + + 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); + } } } From 38ebab0916c933cb35938cade546ecf90366c2eb Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 26 Feb 2021 18:15:59 +0100 Subject: [PATCH 09/22] Add info about how many styles are active and allow for showing inactive styles. --- .../Data/Converters/BoolConverters.cs | 6 +++++ .../ViewModels/ControlDetailsViewModel.cs | 23 +++++++++++++++++++ .../Diagnostics/Views/ControlDetailsView.xaml | 17 +++++++++----- 3 files changed, 40 insertions(+), 6 deletions(-) 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.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 19eb14ba06..32592559e5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -21,6 +21,8 @@ namespace Avalonia.Diagnostics.ViewModels private AvaloniaPropertyViewModel _selectedProperty; private string _styleFilter; private bool _snapshotStyles; + private bool _showInactiveStyles; + private string _styleStatus; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { @@ -154,6 +156,18 @@ namespace Avalonia.Diagnostics.ViewModels 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) @@ -288,9 +302,16 @@ namespace Avalonia.Diagnostics.ViewModels private void UpdateStyles() { + int activeCount = 0; + foreach (var style in AppliedStyles) { style.Update(); + + if (style.IsActive) + { + activeCount++; + } } var propertyBuckets = new Dictionary>(); @@ -330,6 +351,8 @@ namespace Avalonia.Diagnostics.ViewModels { pseudoClass.Update(); } + + StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)"; } private bool FilterProperty(object arg) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index c7a52a5a5f..9ba576c826 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -3,7 +3,8 @@ xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" - x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"> + x:Class="Avalonia.Diagnostics.Views.ControlDetailsView" + x:Name="Main"> @@ -153,9 +154,10 @@ - - - + + + + @@ -165,10 +167,13 @@ - + - + + + + From da9b7c32ac301d01aca98be532a6f2eb5e31948e Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Wed, 3 Mar 2021 12:56:57 +0100 Subject: [PATCH 10/22] Branchless data index check --- src/Avalonia.Layout/ElementManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index bf5a45966b..cb13deb15f 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -207,7 +207,7 @@ namespace Avalonia.Layout } } - public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount; + public bool IsIndexValidInData(int currentIndex) => (uint)currentIndex < _context.ItemCount; public ILayoutable GetRealizedElement(int dataIndex) { From f831b5d346897500ba1d03bb3a46249416ff7d0c Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Wed, 3 Mar 2021 18:19:20 +0100 Subject: [PATCH 11/22] Fixed parameter name in uniform grid state --- src/Avalonia.Layout/UniformGridLayoutState.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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; From 7475354709e597bf21ea0cd6c0145016eefe45ac Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 4 Mar 2021 10:37:41 +0100 Subject: [PATCH 12/22] Add more mono trickery --- .../ExpressionObserverBuilderTests_AttachedProperty.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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); } From 411b8f039843e3c3478569985bd1a701497ace9e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 4 Mar 2021 17:22:40 +0000 Subject: [PATCH 13/22] enumerate child windows and find out if close will be cancelled before closing. --- src/Avalonia.Controls/Window.cs | 38 ++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ab99cfad17..c40787209e 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -482,10 +482,9 @@ namespace Avalonia.Controls try { - if (!ignoreCancel && HandleClosing()) + if (!ignoreCancel && ShouldCancelClose()) { close = false; - return; } } finally @@ -497,11 +496,25 @@ namespace Avalonia.Controls } } + /// + /// Handles a closing notification from . + /// true if closing is cancelled. Otherwise false. + /// + protected virtual bool HandleClosing() + { + if (ShouldCancelClose()) + { + CloseInternal(); + return true; + } + + return false; + } + private void CloseInternal() { foreach (var (child, _) in _children.ToList()) { - // if we HandleClosing() before then there will be no children. child.CloseInternal(); } @@ -515,20 +528,13 @@ namespace Avalonia.Controls PlatformImpl?.Dispose(); } - /// - /// Handles a closing notification from . - /// - protected virtual bool HandleClosing() + private bool ShouldCancelClose() { bool canClose = true; - + foreach (var (child, _) in _children.ToList()) { - if (!child.HandleClosing()) - { - child.CloseInternal(); - } - else + if (child.ShouldCancelClose()) { canClose = false; } @@ -541,10 +547,8 @@ namespace Avalonia.Controls return args.Cancel; } - else - { - return !canClose; - } + + return true; } protected virtual void HandleWindowStateChanged(WindowState state) From 460a5ba5ae30d486ed384fa4b99e93b8a3bfe357 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 4 Mar 2021 17:28:07 +0000 Subject: [PATCH 14/22] whitespace. --- src/Avalonia.Controls/Window.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index c40787209e..e98945777d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -4,10 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; -using Avalonia.Controls.Chrome; using Avalonia.Controls.Platform; -using Avalonia.Controls.Primitives; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; @@ -531,7 +528,7 @@ namespace Avalonia.Controls private bool ShouldCancelClose() { bool canClose = true; - + foreach (var (child, _) in _children.ToList()) { if (child.ShouldCancelClose()) From 19e5b277f25cca790e293c4d1e5ba0c22c3ef22a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 4 Mar 2021 17:51:01 +0000 Subject: [PATCH 15/22] add unit tests. --- .../WindowTests.cs | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index ba29001cf3..6d9b319a3e 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.UnitTests; @@ -137,6 +136,107 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, count); } } + + [Fact] + public void Child_windows_should_be_closed_before_parent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var child = new Window(); + + int count = 0; + int windowClosing = 0; + int childClosing = 0; + int windowClosed = 0; + int childClosed = 0; + + window.Closing += (sender, e) => + { + count++; + windowClosing = count; + }; + + child.Closing += (sender, e) => + { + count++; + childClosing = count; + }; + + window.Closed += (sender, e) => + { + count++; + windowClosed = count; + }; + + child.Closed += (sender, e) => + { + count++; + childClosed = count; + }; + + window.Show(); + child.Show(window); + + window.Close(); + + Assert.Equal(2, windowClosing); + Assert.Equal(1, childClosing); + Assert.Equal(4, windowClosed); + Assert.Equal(3, childClosed); + } + } + + [Fact] + public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var child = new Window(); + + int count = 0; + int windowClosing = 0; + int childClosing = 0; + int windowClosed = 0; + int childClosed = 0; + + window.Closing += (sender, e) => + { + count++; + windowClosing = count; + e.Cancel = true; + }; + + child.Closing += (sender, e) => + { + count++; + childClosing = count; + }; + + window.Closed += (sender, e) => + { + count++; + windowClosed = count; + }; + + child.Closed += (sender, e) => + { + count++; + childClosed = count; + }; + + window.Show(); + child.Show(window); + + window.Close(); + + Assert.Equal(2, windowClosing); + Assert.Equal(1, childClosing); + Assert.Equal(0, windowClosed); + Assert.Equal(0, childClosed); + } + } [Fact] public void Showing_Should_Start_Renderer() From 50a88ae2db60aba84b17a72aedf9256a44830881 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 5 Mar 2021 11:27:43 +0000 Subject: [PATCH 16/22] share cancel event args and ensure event is raised on all windows. --- src/Avalonia.Controls/Window.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index e98945777d..2aca1854ce 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -525,13 +525,18 @@ namespace Avalonia.Controls PlatformImpl?.Dispose(); } - private bool ShouldCancelClose() + private bool ShouldCancelClose(CancelEventArgs args = null) { + if (args is null) + { + args = new CancelEventArgs(); + } + bool canClose = true; foreach (var (child, _) in _children.ToList()) { - if (child.ShouldCancelClose()) + if (child.ShouldCancelClose(args)) { canClose = false; } @@ -539,7 +544,6 @@ namespace Avalonia.Controls if (canClose) { - var args = new CancelEventArgs(); OnClosing(args); return args.Cancel; From 6fecb61da0a328ac64d5014006f00fa3fb551c16 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 5 Mar 2021 14:15:32 +0100 Subject: [PATCH 17/22] Added nullable annotations to Control class. And update a bit of stuff to more modern C#. --- src/Avalonia.Controls/Control.cs | 67 +++++++++++++------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 41370d8464..620f5afa81 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -8,6 +8,8 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -23,20 +25,20 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty> FocusAdornerProperty = - AvaloniaProperty.Register>(nameof(FocusAdorner)); + public static readonly StyledProperty?> FocusAdornerProperty = + AvaloniaProperty.Register?>(nameof(FocusAdorner)); /// /// Defines the property. /// - public static readonly StyledProperty TagProperty = - AvaloniaProperty.Register(nameof(Tag)); + public static readonly StyledProperty TagProperty = + AvaloniaProperty.Register(nameof(Tag)); /// /// Defines the property. /// - public static readonly StyledProperty ContextMenuProperty = - AvaloniaProperty.Register(nameof(ContextMenu)); + public static readonly StyledProperty ContextMenuProperty = + AvaloniaProperty.Register(nameof(ContextMenu)); /// /// Event raised when an element wishes to be scrolled into view. @@ -44,16 +46,16 @@ namespace Avalonia.Controls public static readonly RoutedEvent RequestBringIntoViewEvent = RoutedEvent.Register("RequestBringIntoView", RoutingStrategies.Bubble); - private DataTemplates _dataTemplates; - private IControl _focusAdorner; + private DataTemplates? _dataTemplates; + private IControl? _focusAdorner; /// /// Gets or sets the control's focus adorner. /// - public ITemplate FocusAdorner + public ITemplate? FocusAdorner { - get { return GetValue(FocusAdornerProperty); } - set { SetValue(FocusAdornerProperty, value); } + get => GetValue(FocusAdornerProperty); + set => SetValue(FocusAdornerProperty, value); } /// @@ -63,27 +65,27 @@ namespace Avalonia.Controls /// Each control may define data templates which are applied to the control itself and its /// children. /// - public DataTemplates DataTemplates => _dataTemplates ?? (_dataTemplates = new DataTemplates()); + public DataTemplates DataTemplates => _dataTemplates ??= new DataTemplates(); /// /// Gets or sets a context menu to the control. /// - public ContextMenu ContextMenu + public ContextMenu? ContextMenu { - get { return GetValue(ContextMenuProperty); } - set { SetValue(ContextMenuProperty, value); } + get => GetValue(ContextMenuProperty); + set => SetValue(ContextMenuProperty, value); } /// /// Gets or sets a user-defined object attached to the control. /// - public object Tag + public object? Tag { - get { return GetValue(TagProperty); } - set { SetValue(TagProperty, value); } + get => GetValue(TagProperty); + set => SetValue(TagProperty, value); } - public new IControl Parent => (IControl)base.Parent; + public new IControl? Parent => (IControl?)base.Parent; /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; @@ -106,15 +108,10 @@ namespace Avalonia.Controls { var c = i as IControl; - if (c?.IsInitialized == false) + if (c?.IsInitialized == false && c is ISupportInitialize init) { - var init = c as ISupportInitialize; - - if (init != null) - { - init.BeginInit(); - init.EndInit(); - } + init.BeginInit(); + init.EndInit(); } } } @@ -131,10 +128,7 @@ namespace Avalonia.Controls /// Gets the element that receives the focus adorner. /// /// The control that receives the focus adorner. - protected virtual IControl GetTemplateFocusTarget() - { - return this; - } + protected virtual IControl? GetTemplateFocusTarget() => this; /// protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) @@ -173,15 +167,10 @@ namespace Avalonia.Controls } } - if (_focusAdorner != null) + if (_focusAdorner != null && GetTemplateFocusTarget() is Visual target) { - var target = (Visual)GetTemplateFocusTarget(); - - if (target != null) - { - AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target); - adornerLayer.Children.Add(_focusAdorner); - } + AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target); + adornerLayer.Children.Add(_focusAdorner); } } } From e7c5cb10cb6c3f9b55b41c72fcd2af004964a06f Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Fri, 5 Mar 2021 17:08:09 +0300 Subject: [PATCH 18/22] Fix typo in doc for Visual.cs --- src/Avalonia.Visuals/Visual.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 9327531b46..80d4195421 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -150,7 +150,7 @@ namespace Avalonia public TransformedBounds? TransformedBounds => _transformedBounds; /// - /// Gets a value indicating whether the control should be clipped to its bounds. + /// Gets or sets a value indicating whether the control should be clipped to its bounds. /// public bool ClipToBounds { @@ -191,7 +191,7 @@ namespace Avalonia } /// - /// Gets a value indicating whether this control is visible. + /// Gets or sets a value indicating whether this control is visible. /// public bool IsVisible { @@ -200,7 +200,7 @@ namespace Avalonia } /// - /// Gets the opacity of the control. + /// Gets or sets the opacity of the control. /// public double Opacity { @@ -209,7 +209,7 @@ namespace Avalonia } /// - /// Gets the opacity mask of the control. + /// Gets or sets the opacity mask of the control. /// public IBrush OpacityMask { @@ -218,7 +218,7 @@ namespace Avalonia } /// - /// Gets the render transform of the control. + /// Gets or sets the render transform of the control. /// public ITransform RenderTransform { @@ -227,7 +227,7 @@ namespace Avalonia } /// - /// Gets the transform origin of the control. + /// Gets or sets the transform origin of the control. /// public RelativePoint RenderTransformOrigin { @@ -236,7 +236,7 @@ namespace Avalonia } /// - /// Gets the Z index of the control. + /// Gets or sets the Z index of the control. /// /// /// Controls with a higher will appear in front of controls with From e85da082b0129d02d805ee6c5258d6510e2d993e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 5 Mar 2021 15:02:12 +0000 Subject: [PATCH 19/22] fix handleclosing logic. --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 2aca1854ce..f51cf8b300 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -499,7 +499,7 @@ namespace Avalonia.Controls /// protected virtual bool HandleClosing() { - if (ShouldCancelClose()) + if (!ShouldCancelClose()) { CloseInternal(); return true; From 8b2fb2711a7d07c5583cfcbbfe2d09852d60fb79 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 5 Mar 2021 15:26:17 +0000 Subject: [PATCH 20/22] fix return value. --- src/Avalonia.Controls/Window.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f51cf8b300..98f4cadc13 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -502,10 +502,10 @@ namespace Avalonia.Controls if (!ShouldCancelClose()) { CloseInternal(); - return true; + return false; } - return false; + return true; } private void CloseInternal() From f829137f73b9c6f858449c44d315650ef1f53c06 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 5 Mar 2021 16:04:23 +0000 Subject: [PATCH 21/22] add unit tests for window close when os button is clicked --- .../WindowTests.cs | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 6d9b319a3e..4822c07fa1 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -138,7 +138,112 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Child_windows_should_be_closed_before_parent() + public void Child_windows_should_be_closed_before_parent_OSCloseButton() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var child = new Window(); + + int count = 0; + int windowClosing = 0; + int childClosing = 0; + int windowClosed = 0; + int childClosed = 0; + + window.Closing += (sender, e) => + { + count++; + windowClosing = count; + }; + + child.Closing += (sender, e) => + { + count++; + childClosing = count; + }; + + window.Closed += (sender, e) => + { + count++; + windowClosed = count; + }; + + child.Closed += (sender, e) => + { + count++; + childClosed = count; + }; + + window.Show(); + child.Show(window); + + var cancel = window.PlatformImpl.Closing(); + + Assert.Equal(false, cancel); + + Assert.Equal(2, windowClosing); + Assert.Equal(1, childClosing); + Assert.Equal(4, windowClosed); + Assert.Equal(3, childClosed); + } + } + + [Fact] + public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var child = new Window(); + + int count = 0; + int windowClosing = 0; + int childClosing = 0; + int windowClosed = 0; + int childClosed = 0; + + window.Closing += (sender, e) => + { + count++; + windowClosing = count; + e.Cancel = true; + }; + + child.Closing += (sender, e) => + { + count++; + childClosing = count; + }; + + window.Closed += (sender, e) => + { + count++; + windowClosed = count; + }; + + child.Closed += (sender, e) => + { + count++; + childClosed = count; + }; + + window.Show(); + child.Show(window); + + var cancel = window.PlatformImpl.Closing(); + + Assert.Equal(true, cancel); + + Assert.Equal(2, windowClosing); + Assert.Equal(1, childClosing); + Assert.Equal(0, windowClosed); + Assert.Equal(0, childClosed); + } + } + + [Fact] + public void Child_windows_should_be_closed_before_parent_Programatically() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -188,7 +293,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel() + public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_Programatically() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { From 69a149f0ca1e8749bfa6ccaefe002289f7bbf204 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 5 Mar 2021 16:12:34 +0000 Subject: [PATCH 22/22] use inline data. --- .../WindowTests.cs | 133 ++++-------------- 1 file changed, 25 insertions(+), 108 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 4822c07fa1..e8311b79ac 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -137,8 +137,10 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Child_windows_should_be_closed_before_parent_OSCloseButton() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Child_windows_should_be_closed_before_parent(bool programaticClose) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -178,112 +180,16 @@ namespace Avalonia.Controls.UnitTests window.Show(); child.Show(window); - var cancel = window.PlatformImpl.Closing(); - - Assert.Equal(false, cancel); - - Assert.Equal(2, windowClosing); - Assert.Equal(1, childClosing); - Assert.Equal(4, windowClosed); - Assert.Equal(3, childClosed); - } - } - - [Fact] - public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var window = new Window(); - var child = new Window(); - - int count = 0; - int windowClosing = 0; - int childClosing = 0; - int windowClosed = 0; - int childClosed = 0; - - window.Closing += (sender, e) => - { - count++; - windowClosing = count; - e.Cancel = true; - }; - - child.Closing += (sender, e) => - { - count++; - childClosing = count; - }; - - window.Closed += (sender, e) => + if (programaticClose) { - count++; - windowClosed = count; - }; - - child.Closed += (sender, e) => - { - count++; - childClosed = count; - }; - - window.Show(); - child.Show(window); - - var cancel = window.PlatformImpl.Closing(); - - Assert.Equal(true, cancel); - - Assert.Equal(2, windowClosing); - Assert.Equal(1, childClosing); - Assert.Equal(0, windowClosed); - Assert.Equal(0, childClosed); - } - } - - [Fact] - public void Child_windows_should_be_closed_before_parent_Programatically() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var window = new Window(); - var child = new Window(); - - int count = 0; - int windowClosing = 0; - int childClosing = 0; - int windowClosed = 0; - int childClosed = 0; - - window.Closing += (sender, e) => - { - count++; - windowClosing = count; - }; - - child.Closing += (sender, e) => - { - count++; - childClosing = count; - }; - - window.Closed += (sender, e) => - { - count++; - windowClosed = count; - }; - - child.Closed += (sender, e) => + window.Close(); + } + else { - count++; - childClosed = count; - }; + var cancel = window.PlatformImpl.Closing(); - window.Show(); - child.Show(window); - - window.Close(); + Assert.Equal(false, cancel); + } Assert.Equal(2, windowClosing); Assert.Equal(1, childClosing); @@ -292,8 +198,10 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_Programatically() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton(bool programaticClose) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -334,7 +242,16 @@ namespace Avalonia.Controls.UnitTests window.Show(); child.Show(window); - window.Close(); + if (programaticClose) + { + window.Close(); + } + else + { + var cancel = window.PlatformImpl.Closing(); + + Assert.Equal(true, cancel); + } Assert.Equal(2, windowClosing); Assert.Equal(1, childClosing);