From da38b72a35149bc83fe4fb5acb1d828320baccfe Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 15:17:35 +0200 Subject: [PATCH 01/27] fixes(DevTools): Renamed Type as AssignedType --- .../Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs | 8 ++++---- .../Diagnostics/ViewModels/ClrPropertyViewModel.cs | 6 +++--- .../Diagnostics/ViewModels/PropertyViewModel.cs | 2 +- .../Diagnostics/Views/ControlDetailsView.xaml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index e4c4ca6115..6bd26e39d6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -3,7 +3,7 @@ namespace Avalonia.Diagnostics.ViewModels internal class AvaloniaPropertyViewModel : PropertyViewModel { private readonly AvaloniaObject _target; - private string _type; + private string _assignedType; private object? _value; private string _priority; private string _group; @@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Priority => _priority; - public override string Type => _type; + public override string AssignedType => _assignedType; public override string Value { @@ -56,7 +56,7 @@ namespace Avalonia.Diagnostics.ViewModels if (Property.IsDirect) { RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); + RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); _group = "Properties"; @@ -66,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels var val = _target.GetDiagnostic(Property); RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); + RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); if (val != null) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index 65626aeea5..b87d2b0b53 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -5,7 +5,7 @@ namespace Avalonia.Diagnostics.ViewModels internal class ClrPropertyViewModel : PropertyViewModel { private readonly object _target; - private string _type; + private string _assignedType; private object? _value; #nullable disable @@ -33,7 +33,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Name { get; } public override string Group => "CLR Properties"; - public override string Type => _type; + public override string AssignedType => _assignedType; public override string Value { @@ -60,7 +60,7 @@ namespace Avalonia.Diagnostics.ViewModels { var val = Property.GetValue(_target); RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); + RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index fdbd8c1aa3..080eab072c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -14,7 +14,7 @@ namespace Avalonia.Diagnostics.ViewModels public abstract object Key { get; } public abstract string Name { get; } public abstract string Group { get; } - public abstract string Type { get; } + public abstract string AssignedType { get; } public abstract string Value { get; set; } public abstract string Priority { get; } public abstract bool? IsAttached { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 4b37438993..22ad3374ed 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -33,7 +33,7 @@ - + From 4c3666ba214a9926d6468eb912ec4ea5e424b1d7 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 16:18:36 +0200 Subject: [PATCH 02/27] feat(DevTools): Add PropertyType on ControlDettails --- .../ViewModels/AvaloniaPropertyViewModel.cs | 4 +++ .../ViewModels/ClrPropertyViewModel.cs | 4 +++ .../ViewModels/PropertyViewModel.cs | 26 ++++++++++++++++--- .../Diagnostics/Views/ControlDetailsView.xaml | 1 + 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 6bd26e39d6..68e12b545e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -7,6 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels private object? _value; private string _priority; private string _group; + private readonly string _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -20,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels $"[{property.OwnerType.Name}.{property.Name}]" : property.Name; + _propertyType = GetTypeName(property.PropertyType); Update(); } @@ -50,6 +52,8 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => _group; + public override string PropertyType => _propertyType; + // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index b87d2b0b53..cbbf3e2fbb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -7,6 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels private readonly object _target; private string _assignedType; private object? _value; + private readonly string _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -25,6 +26,8 @@ namespace Avalonia.Diagnostics.ViewModels Name = property.DeclaringType.Name + '.' + property.Name; } + _propertyType = GetTypeName(property.PropertyType); + Update(); } @@ -34,6 +37,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => "CLR Properties"; public override string AssignedType => _assignedType; + public override string PropertyType => _propertyType; public override string Value { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 080eab072c..067693de38 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Globalization; using System.Reflection; +using System.Linq; namespace Avalonia.Diagnostics.ViewModels { @@ -17,8 +18,27 @@ namespace Avalonia.Diagnostics.ViewModels public abstract string AssignedType { get; } public abstract string Value { get; set; } public abstract string Priority { get; } - public abstract bool? IsAttached { get; } - public abstract void Update(); + public abstract bool? IsAttached { get; } + public abstract void Update(); + public abstract string PropertyType { get; } + + + protected static string GetTypeName(Type type) + { + var name = type.Name; + if (Nullable.GetUnderlyingType(type) is Type nullable) + { + name = nullable.Name + "?"; + } + else if (type.IsGenericType) + { + var definition = type.GetGenericTypeDefinition(); + var arguments = type.GetGenericArguments(); + name = definition.Name.Substring(0, definition.Name.IndexOf('`')); + name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>"; + } + return name; + } protected static string ConvertToString(object? value) { @@ -30,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels var converter = TypeDescriptor.GetConverter(value); //CollectionConverter does not deliver any important information. It just displays "(Collection)". - if (!converter.CanConvertTo(typeof(string)) || + if (!converter.CanConvertTo(typeof(string)) || converter.GetType() == typeof(CollectionConverter)) { return value.ToString() ?? "(null)"; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 22ad3374ed..b92ea85227 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -34,6 +34,7 @@ + From 67f83633ce3e7615dd97707e38799c2b769451e5 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 16:26:19 +0200 Subject: [PATCH 03/27] feat(DevTools): Allowed to decode Nullable and Generic Type for AssignedType --- .../Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs | 4 ++-- .../Diagnostics/ViewModels/ClrPropertyViewModel.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 68e12b545e..29e049a5d8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -60,7 +60,7 @@ namespace Avalonia.Diagnostics.ViewModels if (Property.IsDirect) { RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _assignedType, GetTypeName(_value?.GetType() ?? Property.PropertyType), nameof(AssignedType)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); _group = "Properties"; @@ -70,7 +70,7 @@ namespace Avalonia.Diagnostics.ViewModels var val = _target.GetDiagnostic(Property); RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _assignedType, GetTypeName(_value?.GetType() ?? Property.PropertyType), nameof(AssignedType)); if (val != null) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index cbbf3e2fbb..fe48179150 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -64,7 +64,7 @@ namespace Avalonia.Diagnostics.ViewModels { var val = Property.GetValue(_target); RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _assignedType, GetTypeName(_value?.GetType() ?? Property.PropertyType), nameof(AssignedType)); } } } From 3efcd9d259c43d756d5c1ee1ad548d15cac171f2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 16:35:18 +0200 Subject: [PATCH 04/27] feat(DevTools): Allow caching of GetTypeName --- .../ViewModels/PropertyViewModel.cs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 067693de38..1de1c34ea0 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Globalization; using System.Reflection; using System.Linq; +using System.Runtime.CompilerServices; namespace Avalonia.Diagnostics.ViewModels { @@ -11,6 +12,8 @@ namespace Avalonia.Diagnostics.ViewModels private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; private static readonly Type[] StringParameter = { typeof(string) }; private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; + private static readonly ConditionalWeakTable s_getTypeNameCache = + new ConditionalWeakTable(); public abstract object Key { get; } public abstract string Name { get; } @@ -25,17 +28,21 @@ namespace Avalonia.Diagnostics.ViewModels protected static string GetTypeName(Type type) { - var name = type.Name; - if (Nullable.GetUnderlyingType(type) is Type nullable) + if (!s_getTypeNameCache.TryGetValue(type, out var name)) { - name = nullable.Name + "?"; - } - else if (type.IsGenericType) - { - var definition = type.GetGenericTypeDefinition(); - var arguments = type.GetGenericArguments(); - name = definition.Name.Substring(0, definition.Name.IndexOf('`')); - name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>"; + name = type.Name; + if (Nullable.GetUnderlyingType(type) is Type nullable) + { + name = nullable.Name + "?"; + } + else if (type.IsGenericType) + { + var definition = type.GetGenericTypeDefinition(); + var arguments = type.GetGenericArguments(); + name = definition.Name.Substring(0, definition.Name.IndexOf('`')); + name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>"; + } + s_getTypeNameCache.Add(type, name); } return name; } From d0e810305192ab8d058095f759743bae06103628 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 21 Jul 2021 09:25:10 +0200 Subject: [PATCH 05/27] feat(DevTools): Allow toggle PropertyType column visibility --- .../Diagnostics/ViewModels/MainViewModel.cs | 16 ++++++++++++++-- .../Diagnostics/Views/ControlDetailsView.xaml | 5 ++++- .../Diagnostics/Views/MainView.xaml | 10 ++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 3f367165ac..90f127acd2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -22,6 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; + private bool _showPropertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -46,7 +47,7 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeMarginPadding; set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } - + public bool ShouldVisualizeDirtyRects { get => _shouldVisualizeDirtyRects; @@ -151,7 +152,7 @@ namespace Avalonia.Diagnostics.ViewModels get { return _pointerOverElement; } private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } } - + private void UpdateConsoleContext(ConsoleContext context) { context.root = _root; @@ -220,5 +221,16 @@ namespace Avalonia.Diagnostics.ViewModels { StartupScreenIndex = options.StartupScreenIndex; } + + public bool ShowDettailsPropertyType + { + get => _showPropertyType; + private set => RaiseAndSetIfChanged(ref _showPropertyType , value); + } + + public void ToggleShowDettailsPropertyType(object paramter) + { + ShowDettailsPropertyType = !ShowDettailsPropertyType; + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index b92ea85227..5dfc75106a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -34,7 +34,10 @@ - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 8c4db33f91..6d01eb27a1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -15,6 +15,16 @@ IsEnabled="False"/> + + + + + + + + From 739d9de5b7c70d2d35d28063e19663745919ffb4 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Sep 2021 12:32:40 +0200 Subject: [PATCH 06/27] feat(DevTools): Shows the property type and the type assigned to the property in a single or split column. --- .../Diagnostics/ViewModels/PropertyViewModel.cs | 3 +++ .../Diagnostics/Views/ControlDetailsView.xaml | 9 ++++++++- src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 1de1c34ea0..d3cdff6e71 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -24,6 +24,9 @@ namespace Avalonia.Diagnostics.ViewModels public abstract bool? IsAttached { get; } public abstract void Update(); public abstract string PropertyType { get; } + public string Type => PropertyType == AssignedType + ? PropertyType + : $"{PropertyType} {{{AssignedType}}}"; protected static string GetTypeName(Type type) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 5dfc75106a..e6965884b1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -33,7 +33,14 @@ - + + - + Date: Mon, 13 Dec 2021 15:16:13 +0100 Subject: [PATCH 07/27] fixes(DevTools): The Type column does not update when the property value changes. --- .../Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs | 2 ++ .../Diagnostics/ViewModels/ClrPropertyViewModel.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 46f8614273..a09e17ea68 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -45,6 +45,7 @@ namespace Avalonia.Diagnostics.ViewModels { var convertedValue = ConvertFromString(value, Property.PropertyType); _target.SetValue(Property, convertedValue); + Update(); } catch { } } @@ -84,6 +85,7 @@ namespace Avalonia.Diagnostics.ViewModels RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group)); } } + RaisePropertyChanged(nameof(Type)); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index b6bdb00170..e87be9fa36 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -48,6 +48,7 @@ namespace Avalonia.Diagnostics.ViewModels { var convertedValue = ConvertFromString(value, Property.PropertyType); Property.SetValue(_target, convertedValue); + Update(); } catch { } } @@ -67,6 +68,7 @@ namespace Avalonia.Diagnostics.ViewModels var val = Property.GetValue(_target); RaiseAndSetIfChanged(ref _value, val, nameof(Value)); RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + RaisePropertyChanged(nameof(Type)); } } } From 69c1a37372374fa38f80b97996f24a63d88a1024 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sun, 19 Dec 2021 11:18:44 +0200 Subject: [PATCH 08/27] some very initial implementation --- samples/ControlCatalog/App.xaml | 1 + samples/ControlCatalog/App.xaml.cs | 1 - samples/ControlCatalog/MainView.xaml.cs | 10 +-- src/Avalonia.Themes.Fluent/FluentTheme.cs | 77 +++++++++++++++++++++-- 4 files changed, 74 insertions(+), 15 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 227b31bf20..d6ffa04944 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -5,6 +5,7 @@ x:CompileBindings="True" x:Class="ControlCatalog.App"> + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 36b6fc2dcd..085de2ae7a 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -96,7 +96,6 @@ namespace ControlCatalog public override void Initialize() { - Styles.Insert(0, FluentLight); AvaloniaXamlLoader.Load(this); } diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index abedda3c85..3898e9da85 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -11,6 +11,7 @@ using Avalonia.Media.Immutable; using Avalonia.Platform; using ControlCatalog.Pages; using ControlCatalog.Models; +using Avalonia.Themes.Fluent; namespace ControlCatalog { @@ -43,14 +44,7 @@ namespace ControlCatalog { if (themes.SelectedItem is CatalogTheme theme) { - Application.Current.Styles[0] = theme switch - { - CatalogTheme.FluentLight => App.FluentLight, - CatalogTheme.FluentDark => App.FluentDark, - CatalogTheme.DefaultLight => App.DefaultLight, - CatalogTheme.DefaultDark => App.DefaultDark, - _ => Application.Current.Styles[0] - }; + (Application.Current.Styles[0] as FluentTheme).Mode = FluentThemeMode.Dark; } }; diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 43b71567fa..812bd22839 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; #nullable enable @@ -22,6 +23,7 @@ namespace Avalonia.Themes.Fluent private readonly Uri _baseUri; private IStyle[]? _loaded; private bool _isLoading; + private FluentThemeMode _mode; /// /// Initializes a new instance of the class. @@ -44,7 +46,20 @@ namespace Avalonia.Themes.Fluent /// /// Gets or sets the mode of the fluent theme (light, dark). /// - public FluentThemeMode Mode { get; set; } + public FluentThemeMode Mode + { + get => _mode; + set + { + if (_mode != value) + { + _mode = value; + (Loaded as Styles)[3] = FluentDark[0]; + (Loaded as Styles)[4] = FluentDark[1]; + } + + } + } public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; @@ -58,8 +73,25 @@ namespace Avalonia.Themes.Fluent if (_loaded == null) { _isLoading = true; - var loaded = (IStyle)AvaloniaXamlLoader.Load(GetUri(), _baseUri); - _loaded = new[] { loaded }; + Styles? resultStyle = new Styles(); + + resultStyle.AddRange(SharedStyles); + + if (Mode == FluentThemeMode.Light) + { + for (int i = 0; i < FluentLight.Count; i++) + { + resultStyle.Add(FluentLight[i]); + } + } + else if (Mode == FluentThemeMode.Dark) + { + for (int i = 0; i < FluentDark.Count; i++) + { + resultStyle.Add(FluentDark[i]); + } + } + _loaded = new[] { resultStyle }; _isLoading = false; } @@ -105,10 +137,43 @@ namespace Avalonia.Themes.Fluent void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); - private Uri GetUri() => Mode switch + private static Styles SharedStyles = new Styles + { + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Controls/FluentControls.xaml") + } + }; + + private static Styles FluentLight = new Styles { - FluentThemeMode.Dark => new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml", UriKind.Absolute), - _ => new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml", UriKind.Absolute), + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml") + } + }; + private static Styles FluentDark = new Styles + { + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml") + } }; } } From 032b78cd0698215e084f5352159c77706239429a Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sun, 19 Dec 2021 11:34:16 +0200 Subject: [PATCH 09/27] wip --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 812bd22839..73bf5b7226 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -21,7 +21,7 @@ namespace Avalonia.Themes.Fluent public class FluentTheme : IStyle, IResourceProvider { private readonly Uri _baseUri; - private IStyle[]? _loaded; + private IStyle? _loaded; private bool _isLoading; private FluentThemeMode _mode; @@ -91,17 +91,17 @@ namespace Avalonia.Themes.Fluent resultStyle.Add(FluentDark[i]); } } - _loaded = new[] { resultStyle }; + _loaded = resultStyle; _isLoading = false; } - return _loaded?[0]!; + return _loaded; } } bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; - IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); + IReadOnlyList IStyle.Children => _loaded?.Children ?? Array.Empty(); public event EventHandler OwnerChanged { From 9c8b68ff5d0f5fd6cf5a4158b2aa4c093e6d3047 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sun, 19 Dec 2021 12:09:08 +0200 Subject: [PATCH 10/27] more refactorings --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 97 ++++++++++++----------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 73bf5b7226..08d9dc317d 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -21,8 +21,11 @@ namespace Avalonia.Themes.Fluent public class FluentTheme : IStyle, IResourceProvider { private readonly Uri _baseUri; - private IStyle? _loaded; + private Styles _fluentDark = new(); + private Styles _fluentLight = new(); + private Styles _sharedStyles = new(); private bool _isLoading; + private IStyle? _loaded; private FluentThemeMode _mode; /// @@ -32,6 +35,7 @@ namespace Avalonia.Themes.Fluent public FluentTheme(Uri baseUri) { _baseUri = baseUri; + InitStyles(baseUri); } /// @@ -41,6 +45,7 @@ namespace Avalonia.Themes.Fluent public FluentTheme(IServiceProvider serviceProvider) { _baseUri = ((IUriContext)serviceProvider.GetService(typeof(IUriContext))).BaseUri; + InitStyles(_baseUri); } /// @@ -54,8 +59,8 @@ namespace Avalonia.Themes.Fluent if (_mode != value) { _mode = value; - (Loaded as Styles)[3] = FluentDark[0]; - (Loaded as Styles)[4] = FluentDark[1]; + (Loaded as Styles)![1] = _fluentDark[0]; + (Loaded as Styles)![2] = _fluentDark[1]; } } @@ -73,23 +78,17 @@ namespace Avalonia.Themes.Fluent if (_loaded == null) { _isLoading = true; - Styles? resultStyle = new Styles(); - - resultStyle.AddRange(SharedStyles); + Styles? resultStyle = new Styles() { _sharedStyles }; if (Mode == FluentThemeMode.Light) { - for (int i = 0; i < FluentLight.Count; i++) - { - resultStyle.Add(FluentLight[i]); - } + resultStyle.Add(_fluentLight[0]); + resultStyle.Add(_fluentLight[1]); } else if (Mode == FluentThemeMode.Dark) { - for (int i = 0; i < FluentDark.Count; i++) - { - resultStyle.Add(FluentDark[i]); - } + resultStyle.Add(_fluentDark[0]); + resultStyle.Add(_fluentDark[1]); } _loaded = resultStyle; _isLoading = false; @@ -137,43 +136,47 @@ namespace Avalonia.Themes.Fluent void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); - private static Styles SharedStyles = new Styles + private void InitStyles(Uri baseUri) { - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + _sharedStyles = new Styles { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Controls/FluentControls.xaml") - } - }; + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Controls/FluentControls.xaml") + } + }; - private static Styles FluentLight = new Styles - { - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + _fluentLight = new Styles { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml") - } - }; - private static Styles FluentDark = new Styles - { - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml") + } + }; + + _fluentDark = new Styles { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml") - } - }; + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml") + } + }; + } } } From 22be08125735ab59155c549f2a534b29bfbf335e Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sun, 19 Dec 2021 13:10:05 +0200 Subject: [PATCH 11/27] more refactorings --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 08d9dc317d..1c61c03c97 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -59,8 +59,18 @@ namespace Avalonia.Themes.Fluent if (_mode != value) { _mode = value; - (Loaded as Styles)![1] = _fluentDark[0]; - (Loaded as Styles)![2] = _fluentDark[1]; + if (_mode == FluentThemeMode.Dark) + { + (Loaded as Styles)![1] = _fluentDark[0]; + (Loaded as Styles)![2] = _fluentDark[1]; + } + else + { + (Loaded as Styles)![1] = _fluentLight[0]; + (Loaded as Styles)![2] = _fluentLight[1]; + } + + } } From 6ef87ff047f6fddc7afdc2bd2ff073f4141cb313 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 23 Dec 2021 10:23:26 +0100 Subject: [PATCH 12/27] Improve ButtonSpinner OnPointerWheel - Only handle the PointerWheelEvent if the control has the focus - When the pointer wheel should change the selection, mark the event as handled. So any surrounding ScrollViewer does not do the scroll. - Make the Control Focusable by default in Style --- src/Avalonia.Controls/ButtonSpinner.cs | 7 ++++--- src/Avalonia.Themes.Default/ButtonSpinner.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 5fe2cf3704..5a967ab488 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,4 +1,4 @@ -using System; +using System; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -196,13 +196,14 @@ namespace Avalonia.Controls protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { base.OnPointerWheelChanged(e); - if (!e.Handled && AllowSpin) + + if (AllowSpin && IsFocused) { if (e.Delta.Y != 0) { var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true); OnSpin(spinnerEventArgs); - e.Handled = spinnerEventArgs.Handled; + e.Handled = true; } } } diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml index ce2b85d2b5..561ca8bedd 100644 --- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -5,6 +5,7 @@ + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 085de2ae7a..b21b9d80f0 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,12 +16,12 @@ namespace ControlCatalog DataContext = new ApplicationViewModel(); } - private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") }; - private static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + public static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") }; @@ -31,8 +31,7 @@ namespace ControlCatalog new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml") - }, - DataGridFluent + } }; public static Styles FluentLight = new Styles @@ -40,8 +39,7 @@ namespace ControlCatalog new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml") - }, - DataGridFluent + } }; public static Styles DefaultLight = new Styles @@ -65,8 +63,7 @@ namespace ControlCatalog new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") - }, - DataGridDefault + } }; public static Styles DefaultDark = new Styles @@ -90,8 +87,7 @@ namespace ControlCatalog new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") - }, - DataGridDefault + } }; public override void Initialize() diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 3898e9da85..b36c2b6997 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -3,15 +3,12 @@ using System.Collections; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Markup.Xaml.MarkupExtensions; -using Avalonia.Markup.Xaml.Styling; -using Avalonia.Markup.Xaml.XamlIl; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using ControlCatalog.Pages; -using ControlCatalog.Models; using Avalonia.Themes.Fluent; +using ControlCatalog.Models; +using ControlCatalog.Pages; namespace ControlCatalog { @@ -44,7 +41,47 @@ namespace ControlCatalog { if (themes.SelectedItem is CatalogTheme theme) { - (Application.Current.Styles[0] as FluentTheme).Mode = FluentThemeMode.Dark; + var themeStyle = Application.Current.Styles[0]; + if (theme == CatalogTheme.FluentLight) + { + if (themeStyle is FluentTheme fluentTheme) + { + if (fluentTheme.Mode == FluentThemeMode.Dark) + { + fluentTheme.Mode = FluentThemeMode.Light; + } + } + else + { + Application.Current.Styles[0] = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); + Application.Current.Styles[1] = App.DataGridFluent; + } + } + else if (theme == CatalogTheme.FluentDark) + { + if (themeStyle is FluentTheme fluentTheme) + { + if (fluentTheme.Mode == FluentThemeMode.Light) + { + fluentTheme.Mode = FluentThemeMode.Dark; + } + } + else + { + Application.Current.Styles[0] = new FluentTheme(new Uri("avares://ControlCatalog/Styles")) { Mode = FluentThemeMode.Dark }; + Application.Current.Styles[1] = App.DataGridFluent; + } + } + else if (theme == CatalogTheme.DefaultLight) + { + Application.Current.Styles[0] = App.DefaultLight; + Application.Current.Styles[1] = App.DataGridDefault; + } + else if (theme == CatalogTheme.DefaultDark) + { + Application.Current.Styles[0] = App.DefaultDark; + Application.Current.Styles[1] = App.DataGridDefault; + } } }; diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 1c61c03c97..5790de42b1 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -18,7 +18,7 @@ namespace Avalonia.Themes.Fluent /// /// Includes the fluent theme in an application. /// - public class FluentTheme : IStyle, IResourceProvider + public class FluentTheme : AvaloniaObject, IStyle, IResourceProvider { private readonly Uri _baseUri; private Styles _fluentDark = new(); @@ -26,7 +26,6 @@ namespace Avalonia.Themes.Fluent private Styles _sharedStyles = new(); private bool _isLoading; private IStyle? _loaded; - private FluentThemeMode _mode; /// /// Initializes a new instance of the class. @@ -48,31 +47,32 @@ namespace Avalonia.Themes.Fluent InitStyles(_baseUri); } + + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode)); /// /// Gets or sets the mode of the fluent theme (light, dark). /// public FluentThemeMode Mode { - get => _mode; - set + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ModeProperty) { - if (_mode != value) + if (Mode == FluentThemeMode.Dark) { - _mode = value; - if (_mode == FluentThemeMode.Dark) - { - (Loaded as Styles)![1] = _fluentDark[0]; - (Loaded as Styles)![2] = _fluentDark[1]; - } - else - { - (Loaded as Styles)![1] = _fluentLight[0]; - (Loaded as Styles)![2] = _fluentLight[1]; - } - - + (Loaded as Styles)![1] = _fluentDark[0]; + (Loaded as Styles)![2] = _fluentDark[1]; + } + else + { + (Loaded as Styles)![1] = _fluentLight[0]; + (Loaded as Styles)![2] = _fluentLight[1]; } - } } From 5172771a79a032e9df38c25fa3636d042c6e57b2 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 23 Dec 2021 15:53:14 +0200 Subject: [PATCH 15/27] refactoring --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 5790de42b1..53be41e4d1 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -88,23 +88,19 @@ namespace Avalonia.Themes.Fluent if (_loaded == null) { _isLoading = true; - Styles? resultStyle = new Styles() { _sharedStyles }; if (Mode == FluentThemeMode.Light) { - resultStyle.Add(_fluentLight[0]); - resultStyle.Add(_fluentLight[1]); + _loaded = new Styles() { _sharedStyles , _fluentLight[0], _fluentLight[1] }; } else if (Mode == FluentThemeMode.Dark) { - resultStyle.Add(_fluentDark[0]); - resultStyle.Add(_fluentDark[1]); + _loaded = new Styles() { _sharedStyles, _fluentDark[0], _fluentDark[1] }; } - _loaded = resultStyle; _isLoading = false; } - return _loaded; + return _loaded!; } } From c6264191137fff72ca9ffd1f2a1b523d4ef79a7c Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 23 Dec 2021 16:00:43 +0200 Subject: [PATCH 16/27] more refactorings --- samples/ControlCatalog/App.xaml | 2 -- samples/ControlCatalog/App.xaml.cs | 20 +++++--------------- samples/ControlCatalog/MainView.xaml.cs | 4 ++-- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index ab3aa5a4ae..227b31bf20 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -5,8 +5,6 @@ x:CompileBindings="True" x:Class="ControlCatalog.App"> - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index b21b9d80f0..4e86c60285 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; +using Avalonia.Themes.Fluent; using ControlCatalog.ViewModels; namespace ControlCatalog @@ -26,21 +27,9 @@ namespace ControlCatalog Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") }; - public static Styles FluentDark = new Styles - { - new StyleInclude(new Uri("avares://ControlCatalog/Styles")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml") - } - }; + public static FluentTheme FluentDark = new FluentTheme(new Uri("avares://ControlCatalog/Styles")) { Mode = FluentThemeMode.Dark }; - public static Styles FluentLight = new Styles - { - new StyleInclude(new Uri("avares://ControlCatalog/Styles")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml") - } - }; + public static FluentTheme FluentLight = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); public static Styles DefaultLight = new Styles { @@ -92,7 +81,8 @@ namespace ControlCatalog public override void Initialize() { - + Styles.Insert(0, FluentLight); + Styles.Insert(1, DataGridFluent); AvaloniaXamlLoader.Load(this); } diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index b36c2b6997..696062db3f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -53,7 +53,7 @@ namespace ControlCatalog } else { - Application.Current.Styles[0] = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); + Application.Current.Styles[0] = App.FluentLight; Application.Current.Styles[1] = App.DataGridFluent; } } @@ -68,7 +68,7 @@ namespace ControlCatalog } else { - Application.Current.Styles[0] = new FluentTheme(new Uri("avares://ControlCatalog/Styles")) { Mode = FluentThemeMode.Dark }; + Application.Current.Styles[0] = App.FluentDark; Application.Current.Styles[1] = App.DataGridFluent; } } From 7daccf91ae96422fba7208428b1df8e61424a20a Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 23 Dec 2021 17:37:11 +0200 Subject: [PATCH 17/27] cleanup --- samples/ControlCatalog/App.xaml.cs | 6 ++---- samples/ControlCatalog/MainView.xaml.cs | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 4e86c60285..aba22a4af6 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -27,9 +27,7 @@ namespace ControlCatalog Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") }; - public static FluentTheme FluentDark = new FluentTheme(new Uri("avares://ControlCatalog/Styles")) { Mode = FluentThemeMode.Dark }; - - public static FluentTheme FluentLight = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); + public static FluentTheme Fluent = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); public static Styles DefaultLight = new Styles { @@ -81,7 +79,7 @@ namespace ControlCatalog public override void Initialize() { - Styles.Insert(0, FluentLight); + Styles.Insert(0, Fluent); Styles.Insert(1, DataGridFluent); AvaloniaXamlLoader.Load(this); } diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 696062db3f..d36ec3d272 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -53,7 +53,7 @@ namespace ControlCatalog } else { - Application.Current.Styles[0] = App.FluentLight; + Application.Current.Styles[0] = App.Fluent; Application.Current.Styles[1] = App.DataGridFluent; } } @@ -68,7 +68,8 @@ namespace ControlCatalog } else { - Application.Current.Styles[0] = App.FluentDark; + App.Fluent.Mode = FluentThemeMode.Dark; + Application.Current.Styles[0] = App.Fluent; Application.Current.Styles[1] = App.DataGridFluent; } } From 3bfafc5f9397893ca247b149091a1ba02167fd6b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 23 Dec 2021 17:56:30 +0200 Subject: [PATCH 18/27] more cleanup --- samples/ControlCatalog/MainView.xaml.cs | 28 +++++++------------------ 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d36ec3d272..0579355831 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -44,34 +44,22 @@ namespace ControlCatalog var themeStyle = Application.Current.Styles[0]; if (theme == CatalogTheme.FluentLight) { - if (themeStyle is FluentTheme fluentTheme) + if (App.Fluent.Mode != FluentThemeMode.Light) { - if (fluentTheme.Mode == FluentThemeMode.Dark) - { - fluentTheme.Mode = FluentThemeMode.Light; - } - } - else - { - Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.DataGridFluent; + App.Fluent.Mode = FluentThemeMode.Light; } + Application.Current.Styles[0] = App.Fluent; + Application.Current.Styles[1] = App.DataGridFluent; } else if (theme == CatalogTheme.FluentDark) { - if (themeStyle is FluentTheme fluentTheme) - { - if (fluentTheme.Mode == FluentThemeMode.Light) - { - fluentTheme.Mode = FluentThemeMode.Dark; - } - } - else + + if (App.Fluent.Mode != FluentThemeMode.Dark) { App.Fluent.Mode = FluentThemeMode.Dark; - Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.DataGridFluent; } + Application.Current.Styles[0] = App.Fluent; + Application.Current.Styles[1] = App.DataGridFluent; } else if (theme == CatalogTheme.DefaultLight) { From 9e5050cab99a0a55fbfb07ac12998feb18765df1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Dec 2021 00:16:34 +0300 Subject: [PATCH 19/27] Fixed some of Rider's complaints --- src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs | 2 +- .../Media/TextFormatting/Unicode/LineBreakEnumerator.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs index 66223e513d..88b1e3c807 100644 --- a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs +++ b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs @@ -180,7 +180,7 @@ namespace Avalonia.Utilities { var r = _data[c]; if (r?.TryGetTarget(out var sub) == true) - sub.OnEvent(sender, eventArgs); + sub!.OnEvent(sender, eventArgs); else needCompact = true; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 40891a700d..0f6b81975a 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -80,12 +80,12 @@ namespace Avalonia.Media.TextFormatting.Unicode } } - var shouldBreak = GetSimpleBreak() ?? (bool?)GetPairTableBreak(lastClass); + var shouldBreak = GetSimpleBreak() ?? GetPairTableBreak(lastClass); // Rule LB8a _lb8a = _nextClass == LineBreakClass.ZWJ; - if (shouldBreak.Value) + if (shouldBreak) { Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); return true; From 69525b8d55c87481b91c8d22f6f72e088a31d8a8 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 24 Dec 2021 14:43:58 +0100 Subject: [PATCH 20/27] Use IsKeyboradFocusWithin instead of IsFocused --- src/Avalonia.Controls/ButtonSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 5a967ab488..f1c1a55a17 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -197,7 +197,7 @@ namespace Avalonia.Controls { base.OnPointerWheelChanged(e); - if (AllowSpin && IsFocused) + if (AllowSpin && IsKeyboardFocusWithin) { if (e.Delta.Y != 0) { From fe42fbfe6b3d7fcd6951a7aca60749c83b05f42d Mon Sep 17 00:00:00 2001 From: Sergey Volkov Date: Sat, 25 Dec 2021 00:56:20 +0300 Subject: [PATCH 21/27] Add ViewContract to ViewModelViewHost --- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 52 +++++++++++--- .../ViewModelViewHostTest.cs | 72 ++++++++++++++++++- 2 files changed, 113 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index c88323d674..16dee00ebc 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -3,7 +3,7 @@ using System.Reactive.Disposables; using ReactiveUI; using Splat; -namespace Avalonia.ReactiveUI +namespace Avalonia.ReactiveUI { /// /// This content control will automatically load the View associated with @@ -18,6 +18,12 @@ namespace Avalonia.ReactiveUI public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); + /// + /// for the property. + /// + public static readonly StyledProperty ViewContractProperty = + AvaloniaProperty.Register(nameof(ViewContract)); + /// /// Initializes a new instance of the class. /// @@ -25,8 +31,8 @@ namespace Avalonia.ReactiveUI { this.WhenActivated(disposables => { - this.WhenAnyValue(x => x.ViewModel) - .Subscribe(NavigateToViewModel) + this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract) + .Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2)) .DisposeWith(disposables); }); } @@ -39,7 +45,16 @@ namespace Avalonia.ReactiveUI get => GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } - + + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => GetValue(ViewContractProperty); + set => SetValue(ViewContractProperty, value); + } + /// /// Gets or sets the view locator. /// @@ -49,7 +64,8 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - private void NavigateToViewModel(object? viewModel) + /// The contract for view resolution. + private void NavigateToViewModel(object? viewModel, string? contract) { if (viewModel == null) { @@ -57,17 +73,33 @@ namespace Avalonia.ReactiveUI Content = DefaultContent; return; } - + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var viewInstance = viewLocator.ResolveView(viewModel); + var viewInstance = viewLocator.ResolveView(viewModel, contract); if (viewInstance == null) { - this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + if (contract == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + } + else + { + this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); + } + Content = DefaultContent; return; } - - this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + + if (contract == null) + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + } + else + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + } + viewInstance.ViewModel = viewModel; if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index 35d1cbf62d..858c476227 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -12,15 +12,23 @@ namespace Avalonia.ReactiveUI.UnitTests public class FirstView : ReactiveUserControl { } + public class AlternativeFirstView : ReactiveUserControl { } + public class SecondViewModel : ReactiveObject { } public class SecondView : ReactiveUserControl { } + public class AlternativeSecondView : ReactiveUserControl { } + + public static string AlternativeViewContract => "AlternativeView"; + public ViewModelViewHostTest() { Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor)); Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new AlternativeFirstView(), typeof(IViewFor), AlternativeViewContract); + Locator.CurrentMutable.Register(() => new AlternativeSecondView(), typeof(IViewFor), AlternativeViewContract); } [Fact] @@ -67,5 +75,67 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Equal(first, ((FirstView)host.Content).DataContext); Assert.Equal(first, ((FirstView)host.Content).ViewModel); } + + [Fact] + public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel_And_Contract() + { + var defaultContent = new TextBlock(); + var host = new ViewModelViewHost + { + DefaultContent = defaultContent, + PageTransition = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + var first = new FirstViewModel(); + host.ViewModel = first; + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.Equal(typeof(AlternativeFirstView), host.Content.GetType()); + Assert.Equal(first, ((AlternativeFirstView)host.Content).DataContext); + Assert.Equal(first, ((AlternativeFirstView)host.Content).ViewModel); + + var second = new SecondViewModel(); + host.ViewModel = second; + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondView), host.Content.GetType()); + Assert.Equal(second, ((SecondView)host.Content).DataContext); + Assert.Equal(second, ((SecondView)host.Content).ViewModel); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.Equal(typeof(AlternativeSecondView), host.Content.GetType()); + Assert.Equal(second, ((AlternativeSecondView)host.Content).DataContext); + Assert.Equal(second, ((AlternativeSecondView)host.Content).ViewModel); + + host.ViewModel = null; + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + } } -} \ No newline at end of file +} From af902ae969573099b67dc506d7516ca2dde1a165 Mon Sep 17 00:00:00 2001 From: Sergey Volkov Date: Sat, 25 Dec 2021 00:56:36 +0300 Subject: [PATCH 22/27] Add ViewContract to RoutedViewHost --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 57 ++++++++++++--- .../RoutedViewHostTest.cs | 73 +++++++++++++++++++ 2 files changed, 119 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 63456bc13a..a475cf5eac 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -57,7 +57,13 @@ namespace Avalonia.ReactiveUI /// public static readonly StyledProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - + + /// + /// for the property. + /// + public static readonly StyledProperty ViewContractProperty = + AvaloniaProperty.Register(nameof(ViewContract)); + /// /// Initializes a new instance of the class. /// @@ -70,15 +76,18 @@ namespace Avalonia.ReactiveUI .Where(router => router == null)! .Cast(); + var viewContract = this.WhenAnyValue(x => x.ViewContract); + this.WhenAnyValue(x => x.Router) .Where(router => router != null) .SelectMany(router => router!.CurrentViewModel) .Merge(routerRemoved) - .Subscribe(NavigateToViewModel) + .CombineLatest(viewContract) + .Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second)) .DisposeWith(disposables); }); } - + /// /// Gets or sets the of the view model stack. /// @@ -87,17 +96,27 @@ namespace Avalonia.ReactiveUI get => GetValue(RouterProperty); set => SetValue(RouterProperty, value); } - + + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => GetValue(ViewContractProperty); + set => SetValue(ViewContractProperty, value); + } + /// /// Gets or sets the ReactiveUI view locator used by this router. /// public IViewLocator? ViewLocator { get; set; } - + /// /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - private void NavigateToViewModel(object? viewModel) + /// The contract for view resolution. + private void NavigateToViewModel(object? viewModel, string? contract) { if (Router == null) { @@ -112,17 +131,33 @@ namespace Avalonia.ReactiveUI Content = DefaultContent; return; } - + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var viewInstance = viewLocator.ResolveView(viewModel); + var viewInstance = viewLocator.ResolveView(viewModel, contract); if (viewInstance == null) { - this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + if (contract == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + } + else + { + this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); + } + Content = DefaultContent; return; } - - this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + + if (contract == null) + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + } + else + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + } + viewInstance.ViewModel = viewModel; if (viewInstance is IDataContextProvider provider) provider.DataContext = viewModel; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index b82b1b1acc..244b5abc4e 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -29,6 +29,8 @@ namespace Avalonia.ReactiveUI.UnitTests public class FirstRoutableView : ReactiveUserControl { } + public class AlternativeFirstRoutableView : ReactiveUserControl { } + public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel { public string UrlPathSegment => "second"; @@ -38,16 +40,22 @@ namespace Avalonia.ReactiveUI.UnitTests public class SecondRoutableView : ReactiveUserControl { } + public class AlternativeSecondRoutableView : ReactiveUserControl { } + public class ScreenViewModel : ReactiveObject, IScreen { public RoutingState Router { get; } = new RoutingState(); } + public static string AlternativeViewContract => "AlternativeView"; + public RoutedViewHostTest() { Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor)); Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new AlternativeFirstRoutableView(), typeof(IViewFor), AlternativeViewContract); + Locator.CurrentMutable.Register(() => new AlternativeSecondRoutableView(), typeof(IViewFor), AlternativeViewContract); } [Fact] @@ -101,6 +109,71 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Equal(defaultContent, host.Content); } + [Fact] + public void RoutedViewHost_Should_Stay_In_Sync_With_RoutingState_And_Contract() + { + var screen = new ScreenViewModel(); + var defaultContent = new TextBlock(); + var host = new RoutedViewHost + { + Router = screen.Router, + DefaultContent = defaultContent, + PageTransition = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(defaultContent, host.Content); + + var first = new FirstRoutableViewModel(); + screen.Router.Navigate.Execute(first).Subscribe(); + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext); + Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext); + Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel); + + var second = new SecondRoutableViewModel(); + screen.Router.Navigate.Execute(second).Subscribe(); + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(second, ((SecondRoutableView)host.Content).DataContext); + Assert.Equal(second, ((SecondRoutableView)host.Content).ViewModel); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).DataContext); + Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).ViewModel); + + screen.Router.NavigateBack.Execute(Unit.Default).Subscribe(); + + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext); + Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel); + + screen.Router.NavigateBack.Execute(Unit.Default).Subscribe(); + + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(defaultContent, host.Content); + } + [Fact] public void RoutedViewHost_Should_Show_Default_Content_When_Router_Is_Null() { From c39cc1b0833fa74c6a911a75426de1b64c00910a Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Sat, 25 Dec 2021 07:13:23 +0100 Subject: [PATCH 23/27] feat(DevTools): Allow to attach DevTools at Application (#6771) * feat(DevTools): Allow to attach DevTools at Application * fixes(DevTools): PointerOverElement when attach to Application * feat: Add AvaloniaVersion * feat: IsDevelopmentBuild * feat: Add some useful properties to Application Decorator * fixes: Hide Layout Viewer when select appliction node * fix: removed MachineId * fixes: DesignerSupportTests fails * fix(DevTools): Update XML Comment and nits * fix(DevTools): Avoid interaction of Layout Visualizer with keyboard when it is hidden. * Added some comment * fixes: Code formatting * fixes(DevTools): Strip unnecessary property from Application Decorator * fixes(ControlCatalog): remove AttachDevTools from DecoratedWindow * fixes(DevTools): Application doesn't close when the last window closed when DevTools is open * fixes: Missing Application properties decoration * fixes(DevTools): Nullable annotations * fixes(DevTools): Unified the behavior of AttachDevTools * fixes(DevTools): typo * fixes(DevTools): Null Annotation Co-authored-by: Max Katz --- samples/ControlCatalog/App.xaml.cs | 5 + .../ControlCatalog/DecoratedWindow.xaml.cs | 1 - samples/ControlCatalog/MainWindow.xaml.cs | 5 +- .../DevToolsExtensions.cs | 44 +++++++ .../Diagnostics/Behaviors/ColumnDefinition.cs | 47 ++++++++ .../Diagnostics/Controls/Application.cs | 109 ++++++++++++++++++ .../Diagnostics/DevTools.cs | 95 ++++++++++++--- .../Diagnostics/DevToolsOptions.cs | 1 + .../Diagnostics/KeyGestureExtesions.cs | 28 +++++ .../ViewModels/ControlDetailsViewModel.cs | 27 ++--- .../Diagnostics/ViewModels/LogicalTreeNode.cs | 90 ++++++++++++++- .../Diagnostics/ViewModels/MainViewModel.cs | 66 ++++++++--- .../Diagnostics/ViewModels/TreeNode.cs | 9 +- .../ViewModels/TreeNodeCollection.cs | 14 +++ .../Diagnostics/ViewModels/VisualTreeNode.cs | 80 ++++++++++++- .../Diagnostics/Views/ControlDetailsView.xaml | 22 +++- .../Diagnostics/Views/MainView.xaml | 2 +- .../Diagnostics/Views/MainWindow.xaml.cs | 21 ++-- .../Diagnostics/Views/TreePageView.xaml.cs | 7 +- 19 files changed, 593 insertions(+), 80 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index aba22a4af6..816356bee9 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -94,6 +94,11 @@ namespace ControlCatalog singleViewLifetime.MainView = new MainView(); base.OnFrameworkInitializationCompleted(); + + this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreenIndex = 1, + }); } } } diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index bdf5b8fbee..a1383b9107 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -11,7 +11,6 @@ namespace ControlCatalog public DecoratedWindow() { this.InitializeComponent(); - this.AttachDevTools(); } void SetupSide(string name, StandardCursorType cursor, WindowEdge edge) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index dd62698cc7..20591103b7 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,10 +17,7 @@ namespace ControlCatalog public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() - { - StartupScreenIndex = 1, - }); + //Renderer.DrawFps = true; //Renderer.DrawDirtyRects = Renderer.DrawFps = true; diff --git a/src/Avalonia.Diagnostics/DevToolsExtensions.cs b/src/Avalonia.Diagnostics/DevToolsExtensions.cs index aa585dca40..68deaf8335 100644 --- a/src/Avalonia.Diagnostics/DevToolsExtensions.cs +++ b/src/Avalonia.Diagnostics/DevToolsExtensions.cs @@ -37,5 +37,49 @@ namespace Avalonia { DevTools.Attach(root, options); } + + /// + /// Attaches DevTools to a Application, to be opened with the specified options. + /// + /// The Application to attach DevTools to. + public static void AttachDevTools(this Application application) + { + DevTools.Attach(application, new DevToolsOptions()); + } + + /// + /// Attaches DevTools to a Application, to be opened with the specified options. + /// + /// The Application to attach DevTools to. + /// Additional settings of DevTools. + /// + /// Attach DevTools should only be called after application initialization is complete. A good point is + /// + /// + /// + /// public class App : Application + /// { + /// public override void OnFrameworkInitializationCompleted() + /// { + /// if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + /// { + /// desktopLifetime.MainWindow = new MainWindow(); + /// } + /// else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + /// singleViewLifetime.MainView = new MainView(); + /// + /// base.OnFrameworkInitializationCompleted(); + /// this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + /// { + /// StartupScreenIndex = 1, + /// }); + /// } + /// } + /// + /// + public static void AttachDevTools(this Application application, DevToolsOptions options) + { + DevTools.Attach(application, options); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs b/src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs new file mode 100644 index 0000000000..beccf022d7 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs @@ -0,0 +1,47 @@ +namespace Avalonia.Diagnostics.Behaviors +{ + /// + /// See discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773 + /// + static class ColumnDefinition + { + private readonly static Avalonia.Controls.GridLength ZeroWidth = + new Avalonia.Controls.GridLength(0, Avalonia.Controls.GridUnitType.Pixel); + + private readonly static AttachedProperty LastWidthProperty = + AvaloniaProperty.RegisterAttached("LastWidth" + , typeof(ColumnDefinition) + , default); + + public readonly static AttachedProperty IsVisibleProperty = + AvaloniaProperty.RegisterAttached("IsVisible" + , typeof(ColumnDefinition) + , true + , coerce: (element, visibility) => + { + + var lastWidth = element.GetValue(LastWidthProperty); + if (visibility == true && lastWidth is { }) + { + element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, lastWidth); + } + else if (visibility == false) + { + element.SetValue(LastWidthProperty, element.GetValue(Avalonia.Controls.ColumnDefinition.WidthProperty)); + element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, ZeroWidth); + } + return visibility; + } + ); + + public static bool GetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition) + { + return columnDefinition.GetValue(IsVisibleProperty); + } + + public static void SetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition, bool visibility) + { + columnDefinition.SetValue(IsVisibleProperty, visibility); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs new file mode 100644 index 0000000000..b2f2974440 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -0,0 +1,109 @@ +using System; +using Avalonia.Controls; +using Lifetimes = Avalonia.Controls.ApplicationLifetimes; +using App = Avalonia.Application; + +namespace Avalonia.Diagnostics.Controls +{ + class Application : AvaloniaObject + , Input.ICloseable + + { + private readonly App _application; + private static readonly Version s_version = typeof(IAvaloniaObject).Assembly?.GetName()?.Version + ?? Version.Parse("0.0.00"); + public event EventHandler? Closed; + + public Application(App application) + { + _application = application; + + if (_application.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller) + { + EventHandler eh = default!; + eh = (s, e) => + { + controller.Exit -= eh; + Closed?.Invoke(s, e); + }; + controller.Exit += eh; + } + + } + + internal App Instance => _application; + + /// + /// Defines the property. + /// + public object? DataContext => + _application.DataContext; + + /// + /// Gets or sets the application's global data templates. + /// + /// + /// The application's global data templates. + /// + public Avalonia.Controls.Templates.DataTemplates DataTemplates => + _application.DataTemplates; + + /// + /// Gets the application's focus manager. + /// + /// + /// The application's focus manager. + /// + public Input.IFocusManager? FocusManager => + _application.FocusManager; + + /// + /// Gets the application's input manager. + /// + /// + /// The application's input manager. + /// + public Input.InputManager? InputManager => + _application.InputManager; + + /// + /// Gets the application clipboard. + /// + public Input.Platform.IClipboard? Clipboard => + _application.Clipboard; + + /// + /// Gets the application's global resource dictionary. + /// + public IResourceDictionary Resources => + _application.Resources; + + /// + /// Gets the application's global styles. + /// + /// + /// The application's global styles. + /// + /// + /// Global styles apply to all windows in the application. + /// + public Styling.Styles Styles => + _application.Styles; + + /// + /// Application lifetime, use it for things like setting the main window and exiting the app from code + /// Currently supported lifetimes are: + /// - + /// - + /// - + /// + public Lifetimes.IApplicationLifetime? ApplicationLifetime => + _application.ApplicationLifetime; + + /// + /// Application name to be used for various platform-specific purposes + /// + public string? Name => + _application.Name; + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 2a386f106e..683c2e6549 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -1,17 +1,21 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.Views; using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Interactivity; namespace Avalonia.Diagnostics { public static class DevTools { - private static readonly Dictionary s_open = - new Dictionary(); + private static readonly Dictionary s_open = + new Dictionary(); + + private static bool s_attachedToApplication; public static IDisposable Attach(TopLevel root, KeyGesture gesture) { @@ -23,6 +27,11 @@ namespace Avalonia.Diagnostics public static IDisposable Attach(TopLevel root, DevToolsOptions options) { + if (s_attachedToApplication == true) + { + throw new ArgumentException("DevTools already attached to application", nameof(root)); + } + void PreviewKeyDown(object? sender, KeyEventArgs e) { if (options.Gesture.Matches(e)) @@ -37,45 +46,95 @@ namespace Avalonia.Diagnostics RoutingStrategies.Tunnel); } - public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions()); + public static IDisposable Open(TopLevel root) => + Open(Application.Current,new DevToolsOptions(),root as Window); + + public static IDisposable Open(TopLevel root, DevToolsOptions options) => + Open(Application.Current, options, root as Window); - public static IDisposable Open(TopLevel root, DevToolsOptions options) + private static void DevToolsClosed(object? sender, EventArgs e) { - if (s_open.TryGetValue(root, out var window)) + var window = (MainWindow)sender!; + window.Closed -= DevToolsClosed; + if (window.Root is Controls.Application host) + { + s_open.Remove(host.Instance); + } + else { + s_open.Remove(window.Root!); + } + } + + internal static IDisposable Attach(Application? application, DevToolsOptions options, Window? owner = null) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + var result = Disposable.Empty; + // Skip if call on Design Mode + if (!Avalonia.Controls.Design.IsDesignMode + && !s_attachedToApplication) + { + + var lifeTime = application.ApplicationLifetime + as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime; + + if (lifeTime is null) + { + throw new ArgumentNullException(nameof(Application.ApplicationLifetime)); + } + + if (application.InputManager is { }) + { + s_attachedToApplication = true; + + application.InputManager.PreProcess.OfType().Subscribe(e => + { + if (options.Gesture.Matches(e)) + { + result = Open(application, options, owner); + } + }); + + } + } + return result; + } + + private static IDisposable Open(Application? application, DevToolsOptions options, Window? owner = default) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + if (s_open.TryGetValue(application, out var window)) + { window.Activate(); } else { window = new MainWindow { - Root = root, + Root = new Controls.Application(application), Width = options.Size.Width, Height = options.Size.Height, }; window.SetOptions(options); window.Closed += DevToolsClosed; - s_open.Add(root, window); - - if (options.ShowAsChildWindow && root is Window inspectedWindow) + s_open.Add(application, window); + if (options.ShowAsChildWindow && owner is { }) { - window.Show(inspectedWindow); + window.Show(owner); } else { window.Show(); } } - return Disposable.Create(() => window?.Close()); } - - private static void DevToolsClosed(object? sender, EventArgs e) - { - var window = (MainWindow)sender!; - s_open.Remove(window.Root!); - window.Closed -= DevToolsClosed; - } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 5336dca65b..4181989388 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -16,6 +16,7 @@ namespace Avalonia.Diagnostics /// Gets or sets a value indicating whether DevTools should be displayed as a child window /// of the window being inspected. The default value is true. /// + /// This setting is ignored if DevTools is attached to public bool ShowAsChildWindow { get; set; } = true; /// diff --git a/src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs b/src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs new file mode 100644 index 0000000000..bb3ebc4708 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs @@ -0,0 +1,28 @@ +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.Diagnostics +{ + static class KeyGestureExtesions + { + public static bool Matches(this KeyGesture gesture, RawKeyEventArgs keyEvent) => + keyEvent != null && + (KeyModifiers)(keyEvent.Modifiers & RawInputModifiers.KeyboardMask) == gesture.KeyModifiers && + ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(gesture.Key); + + private static Key ResolveNumPadOperationKey(Key key) + { + switch (key) + { + case Key.Add: + return Key.OemPlus; + case Key.Subtract: + return Key.OemMinus; + case Key.Decimal: + return Key.OemPeriod; + default: + return key; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 16293e35d2..c96172d5bb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -16,7 +16,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class ControlDetailsViewModel : ViewModelBase, IDisposable { - private readonly IVisual _control; + private readonly IAvaloniaObject _avaloniaObject; private IDictionary>? _propertyIndex; private PropertyViewModel? _selectedProperty; private DataGridCollectionView? _propertiesView; @@ -28,20 +28,21 @@ namespace Avalonia.Diagnostics.ViewModels private string? _selectedEntityName; private string? _selectedEntityType; - public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) + public ControlDetailsViewModel(TreePageViewModel treePage, IAvaloniaObject avaloniaObject) { - _control = control; + _avaloniaObject = avaloniaObject; - TreePage = treePage; - - Layout = new ControlLayoutViewModel(control); + TreePage = treePage; + Layout = avaloniaObject is IVisual + ? new ControlLayoutViewModel((IVisual)avaloniaObject) + : default; - NavigateToProperty(control, (control as IControl)?.Name ?? control.ToString()); + NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); AppliedStyles = new ObservableCollection(); PseudoClasses = new ObservableCollection(); - if (control is StyledElement styledElement) + if (avaloniaObject is StyledElement styledElement) { styledElement.Classes.CollectionChanged += OnClassesChanged; @@ -181,7 +182,7 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _styleStatus, value); } - public ControlLayoutViewModel Layout { get; } + public ControlLayoutViewModel? Layout { get; } protected override void OnPropertyChanged(PropertyChangedEventArgs e) { @@ -215,17 +216,17 @@ namespace Avalonia.Diagnostics.ViewModels public void Dispose() { - if (_control is INotifyPropertyChanged inpc) + if (_avaloniaObject is INotifyPropertyChanged inpc) { inpc.PropertyChanged -= ControlPropertyChanged; } - if (_control is AvaloniaObject ao) + if (_avaloniaObject is AvaloniaObject ao) { ao.PropertyChanged -= ControlPropertyChanged; } - if (_control is StyledElement se) + if (_avaloniaObject is StyledElement se) { se.Classes.CollectionChanged -= OnClassesChanged; } @@ -278,7 +279,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - Layout.ControlPropertyChanged(sender, e); + Layout?.ControlPropertyChanged(sender, e); } private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index 04215fa8ae..d627eba154 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -1,23 +1,31 @@ using System; +using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; +using Lifetimes = Avalonia.Controls.ApplicationLifetimes; +using System.Linq; namespace Avalonia.Diagnostics.ViewModels { internal class LogicalTreeNode : TreeNode { - public LogicalTreeNode(ILogical logical, TreeNode? parent) - : base((Control)logical, parent) + public LogicalTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent) + : base(avaloniaObject, parent) { - Children = new LogicalTreeNodeCollection(this, logical); + Children = avaloniaObject switch + { + ILogical logical => new LogicalTreeNodeCollection(this, logical), + Controls.Application host => new ApplicationHostLogical(this, host), + _ => TreeNodeCollection.Empty + }; } public override TreeNodeCollection Children { get; } public static LogicalTreeNode[] Create(object control) { - var logical = control as ILogical; + var logical = control as IAvaloniaObject; return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty(); } @@ -41,10 +49,82 @@ namespace Avalonia.Diagnostics.ViewModels protected override void Initialize(AvaloniaList nodes) { _subscription = _control.LogicalChildren.ForEachItem( - (i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)), + (i, item) => nodes.Insert(i, new LogicalTreeNode((IAvaloniaObject)item, Owner)), (i, item) => nodes.RemoveAt(i), () => nodes.Clear()); } } + + internal class ApplicationHostLogical : TreeNodeCollection + { + readonly Controls.Application _application; + CompositeDisposable _subscriptions = new CompositeDisposable(2); + public ApplicationHostLogical(TreeNode owner, Controls.Application host) : + base(owner) + { + _application = host; + } + + protected override void Initialize(AvaloniaList nodes) + { + if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single) + { + nodes.Add(new LogicalTreeNode(single.MainView, Owner)); + } + if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic) + { + + for (int i = 0; i < classic.Windows.Count; i++) + { + var window = classic.Windows[i]; + if (window is Views.MainWindow) + { + continue; + } + nodes.Add(new LogicalTreeNode(window, Owner)); + } + _subscriptions = new System.Reactive.Disposables.CompositeDisposable() + { + Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> + { + if (s is Views.MainWindow) + { + return; + } + nodes.Add(new LogicalTreeNode((IAvaloniaObject)s!,Owner)); + }), + Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=> + { + if (s is Views.MainWindow) + { + return; + } + var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s)); + if(!(item is null)) + { + nodes.Remove(item); + } + if(nodes.Count == 0) + { + if (Avalonia.Application.Current?.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller) + { + controller.Shutdown(); + } + else + { + Environment.Exit(0); + } + } + }), + }; + } + } + + public override void Dispose() + { + _subscriptions?.Dispose(); + base.Dispose(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 74f2f05948..d809768a47 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -4,12 +4,14 @@ using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Threading; +using System.Reactive.Linq; +using System.Linq; namespace Avalonia.Diagnostics.ViewModels { internal class MainViewModel : ViewModelBase, IDisposable { - private readonly TopLevel _root; + private readonly AvaloniaObject _root; private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _visualTree; private readonly EventsPageViewModel _events; @@ -17,13 +19,14 @@ namespace Avalonia.Diagnostics.ViewModels private ViewModelBase? _content; private int _selectedTab; private string? _focusedControl; - private string? _pointerOverElement; + private IInputElement? _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; private bool _freezePopups; - - public MainViewModel(TopLevel root) + private string? _pointerOverElementName; + private IInputRoot? _pointerOverRoot; + public MainViewModel(AvaloniaObject root) { _root = root; _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); @@ -35,8 +38,24 @@ namespace Avalonia.Diagnostics.ViewModels if (KeyboardDevice.Instance is not null) KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged; SelectedTab = 0; - _pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty) - .Subscribe(x => PointerOverElement = x?.GetType().Name); + if (root is TopLevel topLevel) + { + _pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty) + .Subscribe(x => PointerOverElement = x); + + } + else + { +#nullable disable + _pointerOverSubscription = InputManager.Instance.PreProcess + .OfType() + .Subscribe(e => + { + PointerOverRoot = e.Root; + PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault(); + }); +#nullable restore + } Console = new ConsoleViewModel(UpdateConsoleContext); } @@ -51,13 +70,13 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeMarginPadding; set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } - + public bool ShouldVisualizeDirtyRects { get => _shouldVisualizeDirtyRects; set { - _root.Renderer.DrawDirtyRects = value; + ((TopLevel)_root).Renderer.DrawDirtyRects = value; RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); } } @@ -77,7 +96,7 @@ namespace Avalonia.Diagnostics.ViewModels get => _showFpsOverlay; set { - _root.Renderer.DrawFps = value; + ((TopLevel)_root).Renderer.DrawFps = value; RaiseAndSetIfChanged(ref _showFpsOverlay, value); } } @@ -150,12 +169,28 @@ namespace Avalonia.Diagnostics.ViewModels private set { RaiseAndSetIfChanged(ref _focusedControl, value); } } - public string? PointerOverElement + public IInputRoot? PointerOverRoot + { + get => _pointerOverRoot; + private set => RaiseAndSetIfChanged( ref _pointerOverRoot , value); + } + + public IInputElement? PointerOverElement { get { return _pointerOverElement; } - private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } + private set + { + RaiseAndSetIfChanged(ref _pointerOverElement, value); + PointerOverElementName = value?.GetType()?.Name; + } } - + + public string? PointerOverElementName + { + get => _pointerOverElementName; + private set => RaiseAndSetIfChanged(ref _pointerOverElementName, value); + } + private void UpdateConsoleContext(ConsoleContext context) { context.root = _root; @@ -188,8 +223,11 @@ namespace Avalonia.Diagnostics.ViewModels _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); - _root.Renderer.DrawDirtyRects = false; - _root.Renderer.DrawFps = false; + if (_root is TopLevel top) + { + top.Renderer.DrawDirtyRects = false; + top.Renderer.DrawFps = false; + } } private void UpdateFocusedControl() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 94707ac189..5459810db9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -16,12 +16,13 @@ namespace Avalonia.Diagnostics.ViewModels private string _classes; private bool _isExpanded; - protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null) + protected TreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null) { _classes = string.Empty; Parent = parent; - Type = customName ?? visual.GetType().Name; - Visual = visual; + var visual = avaloniaObject ; + Type = customName ?? avaloniaObject.GetType().Name; + Visual = visual!; FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; if (visual is IControl control) @@ -76,7 +77,7 @@ namespace Avalonia.Diagnostics.ViewModels get; } - public IVisual Visual + public IAvaloniaObject Visual { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs index c007411f49..953afa280b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -10,6 +10,20 @@ namespace Avalonia.Diagnostics.ViewModels { internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable { + private class EmptyTreeNodeCollection : TreeNodeCollection + { + public EmptyTreeNodeCollection():base(default!) + { + + } + protected override void Initialize(AvaloniaList nodes) + { + + } + } + + static readonly internal TreeNodeCollection Empty = new EmptyTreeNodeCollection(); + private AvaloniaList? _inner; public TreeNodeCollection(TreeNode owner) => Owner = owner; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 6a430897ba..5e1128d4b7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -7,15 +7,22 @@ using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; +using Lifetimes = Avalonia.Controls.ApplicationLifetimes; +using System.Linq; namespace Avalonia.Diagnostics.ViewModels { internal class VisualTreeNode : TreeNode { - public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null) - : base(visual, parent, customName) + public VisualTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null) + : base(avaloniaObject, parent, customName) { - Children = new VisualTreeNodeCollection(this, visual); + Children = avaloniaObject switch + { + IVisual visual => new VisualTreeNodeCollection(this, visual), + Controls.Application host => new ApplicationHostVisuals(this, host), + _ => TreeNodeCollection.Empty + }; if (Visual is IStyleable styleable) IsInTemplate = styleable.TemplatedParent != null; @@ -27,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels public static VisualTreeNode[] Create(object control) { - return control is IVisual visual ? + return control is IAvaloniaObject visual ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); } @@ -130,7 +137,7 @@ namespace Avalonia.Diagnostics.ViewModels _subscriptions.Add( _control.VisualChildren.ForEachItem( - (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), + (i, item) => nodes.Insert(i, new VisualTreeNode((IAvaloniaObject)item, Owner)), (i, item) => nodes.RemoveAt(i), () => nodes.Clear())); } @@ -147,5 +154,68 @@ namespace Avalonia.Diagnostics.ViewModels public string? CustomName { get; } } } + + internal class ApplicationHostVisuals : TreeNodeCollection + { + readonly Controls.Application _application; + CompositeDisposable _subscriptions = new CompositeDisposable(2); + public ApplicationHostVisuals(TreeNode owner, Controls.Application host) : + base(owner) + { + _application = host; + } + + protected override void Initialize(AvaloniaList nodes) + { + if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single) + { + nodes.Add(new VisualTreeNode(single.MainView, Owner)); + } + if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic) + { + + for (int i = 0; i < classic.Windows.Count; i++) + { + var window = classic.Windows[i]; + if (window is Views.MainWindow) + { + continue; + } + nodes.Add(new VisualTreeNode(window, Owner)); + } + _subscriptions = new System.Reactive.Disposables.CompositeDisposable() + { + Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> + { + if (s is Views.MainWindow) + { + return; + } + nodes.Add(new VisualTreeNode((IAvaloniaObject)s!,Owner)); + }), + Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=> + { + if (s is Views.MainWindow) + { + return; + } + var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s)); + if(!(item is null)) + { + nodes.Remove(item); + } + }), + }; + + + } + } + + public override void Dispose() + { + _subscriptions?.Dispose(); + base.Dispose(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index cf74fa1b34..7f924af144 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -4,6 +4,7 @@ xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" + xmlns:lb="using:Avalonia.Diagnostics.Behaviors" x:Class="Avalonia.Diagnostics.Views.ControlDetailsView" x:Name="Main"> @@ -11,8 +12,20 @@ - - + + + + + + + + @@ -53,9 +66,10 @@ - + - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 6f2ac96a66..7e0dab1d6e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -73,7 +73,7 @@ Pointer Over: - + _frozenPopupStates; - private TopLevel? _root; + private AvaloniaObject? _root; public MainWindow() { @@ -50,23 +50,23 @@ namespace Avalonia.Diagnostics.Views this.Opened += lh; } - public TopLevel? Root + public AvaloniaObject? Root { get => _root; set { if (_root != value) { - if (_root != null) + if (_root is ICloseable oldClosable) { - _root.Closed -= RootClosed; + oldClosable.Closed -= RootClosed; } _root = value; - if (_root != null) + if (_root is ICloseable newClosable) { - _root.Closed += RootClosed; + newClosable.Closed += RootClosed; DataContext = new MainViewModel(_root); } else @@ -91,9 +91,9 @@ namespace Avalonia.Diagnostics.Views _frozenPopupStates.Clear(); - if (_root != null) + if (_root is ICloseable cloneable) { - _root.Closed -= RootClosed; + cloneable.Closed -= RootClosed; _root = null; } @@ -123,7 +123,7 @@ namespace Avalonia.Diagnostics.Views .FirstOrDefault(); } - private static List GetPopupRoots(IVisual root) + private static List GetPopupRoots(TopLevel root) { var popupRoots = new List(); @@ -160,7 +160,8 @@ namespace Avalonia.Diagnostics.Views return; } - var root = Root; + var root = Root as TopLevel + ?? vm.PointerOverRoot as TopLevel; if (root is null) { return; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 3543b1adea..649b65c521 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -47,7 +47,12 @@ namespace Avalonia.Diagnostics.Views return; } - var visual = (Visual)node.Visual; + var visual = node.Visual as Visual; + + if (visual is null) + { + return; + } _currentLayer = AdornerLayer.GetAdornerLayer(visual); From 1d34536236ee84dace3f8414ff3a73b71cbac654 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sun, 26 Dec 2021 22:47:31 +0300 Subject: [PATCH 24/27] [WASM] Fix wrong DPI value --- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index 3e9849d4dc..acd23a98b0 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -142,7 +142,7 @@ namespace Avalonia.Web.Blazor public Size ClientSize => _clientSize; public Size? FrameSize => null; - public double RenderScaling => 1; + public double RenderScaling => _currentSurface?.Scaling ?? 1; public IEnumerable Surfaces => new object[] { _currentSurface! }; From 215425c45a02bd7c0cad7a881d198a4fbac66568 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 27 Dec 2021 12:17:37 +0100 Subject: [PATCH 25/27] fixes(DevTools): NRE when click 'Visualizze dirty rects' and 'Show fps overly' in Option menu --- .../Diagnostics/Controls/Application.cs | 12 ++++++- .../Diagnostics/ViewModels/MainViewModel.cs | 32 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index b2f2974440..316e31b37e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -28,7 +28,12 @@ namespace Avalonia.Diagnostics.Controls }; controller.Exit += eh; } - + RendererRoot = application.ApplicationLifetime switch + { + Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow.Renderer, + Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as VisualTree.IVisual)?.VisualRoot?.Renderer, + _ => null + }; } internal App Instance => _application; @@ -105,5 +110,10 @@ namespace Avalonia.Diagnostics.Controls /// public string? Name => _application.Name; + + /// + /// Gets the root of the visual tree, if the control is attached to a visual tree. + /// + internal Rendering.IRenderer? RendererRoot { get; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index d809768a47..c607542991 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -76,7 +76,20 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeDirtyRects; set { - ((TopLevel)_root).Renderer.DrawDirtyRects = value; + var changed = true; + if (_root is TopLevel topLevel && topLevel.Renderer is { }) + { + topLevel.Renderer.DrawDirtyRects = value; + } + else if (_root is Controls.Application app && app.RendererRoot is { }) + { + app.RendererRoot.DrawDirtyRects = value; + } + else + { + changed = false; + } + if (changed) RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); } } @@ -96,8 +109,21 @@ namespace Avalonia.Diagnostics.ViewModels get => _showFpsOverlay; set { - ((TopLevel)_root).Renderer.DrawFps = value; - RaiseAndSetIfChanged(ref _showFpsOverlay, value); + var changed = true; + if (_root is TopLevel topLevel && topLevel.Renderer is { }) + { + topLevel.Renderer.DrawFps = value; + } + else if (_root is Controls.Application app && app.RendererRoot is { }) + { + app.RendererRoot.DrawFps = value; + } + else + { + changed = false; + } + if(changed) + RaiseAndSetIfChanged(ref _showFpsOverlay, value); } } From 05021506e6165a004c7eec97e97318bc31203bde Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 27 Dec 2021 11:17:22 -0500 Subject: [PATCH 26/27] Update src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml --- src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 22f9ccf513..381fc68cf3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -15,7 +15,7 @@ IsEnabled="False" /> - + Date: Wed, 29 Dec 2021 01:44:11 +0700 Subject: [PATCH 27/27] select text when clicking on a TextBox with shift key modifier --- src/Avalonia.Controls/TextBox.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 32428bea53..20d8a94c1a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1006,13 +1006,28 @@ namespace Avalonia.Controls if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border)) { var point = e.GetPosition(_presenter); - var index = CaretIndex = _presenter.GetCaretIndex(point); + var index = _presenter.GetCaretIndex(point); + var clickToSelect = index != CaretIndex && e.KeyModifiers.HasFlag(KeyModifiers.Shift); + + if (!clickToSelect) + { + CaretIndex = index; + } + #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) #pragma warning restore CS0618 // Type or member is obsolete { case 1: - SelectionStart = SelectionEnd = index; + if (clickToSelect) + { + SelectionStart = Math.Min(index, CaretIndex); + SelectionEnd = Math.Max(index, CaretIndex); + } + else + { + SelectionStart = SelectionEnd = index; + } break; case 2: if (!StringUtils.IsStartOfWord(text, index))