From f2c3805d6a0086ce27ac9ecb55680def9f31b9ea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Feb 2023 18:08:13 +0100 Subject: [PATCH 001/108] Update ncrunch config. --- .ncrunch/Avalonia.UnitTests.v3.ncrunchproject | 5 +++++ .ncrunch/GpuInterop.v3.ncrunchproject | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .ncrunch/Avalonia.UnitTests.v3.ncrunchproject create mode 100644 .ncrunch/GpuInterop.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..cff5044edf --- /dev/null +++ b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/GpuInterop.v3.ncrunchproject b/.ncrunch/GpuInterop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/GpuInterop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From 1e926dac72b4c666d4c213cd72693d4cbf551e27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Feb 2023 18:19:40 +0100 Subject: [PATCH 002/108] Added failing tests for #10110. --- .../AvaloniaObjectTests_Binding.cs | 18 +++++++++++++ .../Xaml/ResourceDictionaryTests.cs | 25 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 030d6ba215..f0386615c5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -1285,6 +1285,24 @@ namespace Avalonia.Base.UnitTests subscription.Dispose(); } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Binding_Producing_UnsetValue_Does_Not_Cause_Unsubscribe(BindingPriority priority) + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source, priority); + + source.OnNext("foo"); + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + source.OnNext(BindingValue.Unset); + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); + source.OnNext("bar"); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Produces_Correct_Values_And_Base_Values_With_Multiple_Animation_Bindings() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 6cab83751f..bea6bc4dc8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -307,7 +307,30 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.True(buttonResources.ContainsDeferredKey("Red")); } } - + + [Fact] + public void Dynamically_Changing_Referenced_Resources_Works_With_DynamicResource() + { + var xaml = @" + + + Red + + +"; + + var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + + userControl.Resources.Remove("color"); + Assert.Equal(default, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + + userControl.Resources.Add("color", Colors.Blue); + Assert.Equal(Colors.Blue, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( From 7b00ef69892358650c10e0f27fb0d5dd1477e3f5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 12:00:40 +0100 Subject: [PATCH 003/108] Make untyped bindings produce default value. This changes the behavior of bindings slightly, in that previously a binding which produced `UnsetValue` (or a binding error) caused the value of any lower priority value of value to take effect. After this change, the binding reverts to the property's default value. This behavior more closely matches WPF and fixes #10110. --- src/Avalonia.Base/AvaloniaObject.cs | 4 +- .../PropertyStore/BindingEntryBase.cs | 47 +++++++++---------- .../LocalValueBindingObserver.cs | 42 ++++++++++++----- .../LocalValueUntypedBindingObserver.cs | 25 +++++++--- .../SourceUntypedBindingEntry.cs | 2 + .../PropertyStore/TypedBindingEntry.cs | 2 + .../PropertyStore/UntypedBindingEntry.cs | 5 ++ .../AvaloniaObjectTests_Binding.cs | 18 ++++++- .../AvaloniaObjectTests_Validation.cs | 4 +- 9 files changed, 99 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 1946d4ba5c..db6dbe98e0 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -270,8 +270,8 @@ namespace Avalonia /// The property. /// True if the property is set, otherwise false. /// - /// Checks whether a value is assigned to the property, or that there is a binding to the - /// property that is producing a value other than . + /// Returns true if is a styled property which has a value + /// assigned to it or a binding targeting it; otherwise false. /// public bool IsSet(AvaloniaProperty property) { diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index 11dc80ef8f..e1ff0970c2 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -16,6 +16,8 @@ namespace Avalonia.PropertyStore private IDisposable? _subscription; private bool _hasValue; private TValue? _value; + private TValue? _defaultValue; + private bool _isDefaultValueInitialized; protected BindingEntryBase( ValueFrame frame, @@ -89,6 +91,7 @@ namespace Avalonia.PropertyStore protected abstract BindingValue ConvertAndValidate(TSource value); protected abstract BindingValue ConvertAndValidate(BindingValue value); + protected abstract TValue GetDefaultValue(Type ownerType); protected virtual void Start(bool produceValue) { @@ -104,17 +107,6 @@ namespace Avalonia.PropertyStore }; } - private void ClearValue() - { - if (_hasValue) - { - _hasValue = false; - _value = default; - if (_subscription is not null) - Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); - } - } - private void SetValue(BindingValue value) { static void Execute(BindingEntryBase instance, BindingValue value) @@ -124,24 +116,20 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); - if (value.HasValue) - { - if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value)) - { - instance._value = value.Value; - instance._hasValue = true; - if (instance._subscription is not null && instance._subscription != s_creatingQuiet) - instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); - } - } - else if (value.Type != BindingValueType.DoNothing) + var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + + if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, effectiveValue)) { - instance.ClearValue(); + instance._value = effectiveValue; + instance._hasValue = true; if (instance._subscription is not null && instance._subscription != s_creatingQuiet) - instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority); + instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); } } + if (value.Type == BindingValueType.DoNothing) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -161,5 +149,16 @@ namespace Avalonia.PropertyStore _subscription = null; Frame.OnBindingCompleted(this); } + + private TValue GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index f89cb029b6..0771ade647 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -10,6 +10,8 @@ namespace Avalonia.PropertyStore { private readonly ValueStore _owner; private IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; public LocalValueBindingObserver(ValueStore owner, StyledProperty property) { @@ -41,26 +43,28 @@ namespace Avalonia.PropertyStore public void OnNext(T value) { - static void Execute(ValueStore owner, StyledProperty property, T value) + static void Execute(LocalValueBindingObserver instance, T value) { - if (property.ValidateValue?.Invoke(value) != false) - owner.SetValue(property, value, BindingPriority.LocalValue); - else - owner.ClearLocalValue(property); + var owner = instance._owner; + var property = instance.Property; + + if (property.ValidateValue?.Invoke(value) == false) + value = instance.GetCachedDefaultValue(); + + owner.SetValue(property, value, BindingPriority.LocalValue); } if (Dispatcher.UIThread.CheckAccess()) { - Execute(_owner, Property, value); + Execute(this, value); } else { // To avoid allocating closure in the outer scope we need to capture variables // locally. This allows us to skip most of the allocations when on UI thread. - var instance = _owner; - var property = Property; + var instance = this; var newValue = value; - Dispatcher.UIThread.Post(() => Execute(instance, property, newValue)); + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } @@ -73,12 +77,13 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(owner.Owner, property, value); - if (value.HasValue) - owner.SetValue(property, value.Value, BindingPriority.LocalValue); - else if (value.Type != BindingValueType.DataValidationError) - owner.ClearLocalValue(property); + var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); } + if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -92,5 +97,16 @@ namespace Avalonia.PropertyStore Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 2d157b2519..46e6ed810a 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -1,5 +1,4 @@ using System; -using System.Security.Cryptography; using Avalonia.Data; using Avalonia.Threading; @@ -10,6 +9,8 @@ namespace Avalonia.PropertyStore { private readonly ValueStore _owner; private IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty property) { @@ -49,11 +50,7 @@ namespace Avalonia.PropertyStore if (value == AvaloniaProperty.UnsetValue) { - owner.ClearLocalValue(property); - } - else if (value == BindingOperations.DoNothing) - { - // Do nothing! + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); } else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) { @@ -61,11 +58,14 @@ namespace Avalonia.PropertyStore } else { - owner.ClearLocalValue(property); + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); } } + if (value == BindingOperations.DoNothing) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -79,5 +79,16 @@ namespace Avalonia.PropertyStore Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs index b56d0d4529..b82714817b 100644 --- a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs @@ -31,5 +31,7 @@ namespace Avalonia.PropertyStore { throw new NotSupportedException(); } + + protected override TTarget GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); } } diff --git a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs index 697725c87b..550f5c0001 100644 --- a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs @@ -48,5 +48,7 @@ namespace Avalonia.PropertyStore return value; } + + protected override T GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); } } diff --git a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs index f8becb2e06..a77d7fddb6 100644 --- a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs @@ -29,5 +29,10 @@ namespace Avalonia.PropertyStore { throw new NotSupportedException(); } + + protected override object? GetDefaultValue(Type ownerType) + { + return ((IStyledPropertyMetadata)Property.GetMetadata(ownerType)).DefaultValue; + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index f0386615c5..baaed5104d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -372,6 +372,20 @@ namespace Avalonia.Base.UnitTests Assert.Null(target.GetValue(property)); } + [Fact] + public void LocalValue_Bind_Generic_To_ValueType_Accepts_UnsetValue() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(BindingValue.Unset); + + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); + } + [Fact] public void LocalValue_Bind_NonGeneric_To_ValueType_Accepts_UnsetValue() { @@ -383,7 +397,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(AvaloniaProperty.UnsetValue); Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); - Assert.False(target.IsSet(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); } [Fact] @@ -397,7 +411,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(AvaloniaProperty.UnsetValue); Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); - Assert.False(target.IsSet(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index e8175cf477..3fc950573d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -66,7 +66,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Reverts_To_Lower_Priority_If_Style_Binding_Fails_Validation() + public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation_2() { var target = new Class1(); var source = new Subject(); @@ -75,7 +75,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.FooProperty, source, BindingPriority.StyleTrigger); source.OnNext(150); - Assert.Equal(10, target.GetValue(Class1.FooProperty)); + Assert.Equal(11, target.GetValue(Class1.FooProperty)); } [Fact] From bca57ea0e9c65c5b17422303261b1a7a058d842b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 16:27:48 +0100 Subject: [PATCH 004/108] Added additional failing test. --- .../AvaloniaObjectTests_Validation.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index 3fc950573d..513aeb65ab 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -78,6 +78,20 @@ namespace Avalonia.Base.UnitTests Assert.Equal(11, target.GetValue(Class1.FooProperty)); } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation_3(BindingPriority priority) + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source, priority); + source.OnNext(150); + + Assert.Equal(11, target.GetValue(Class1.FooProperty)); + } + [Fact] public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings() { From b2c1d80f5cfdf3c0dccbed07cb908b69f7db3e2d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 16:32:02 +0100 Subject: [PATCH 005/108] Fix validation of local value bindings. Local value bindings with a type of `BindingValue` were not being validated. --- .../PropertyStore/LocalValueBindingObserver.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 0771ade647..5908d9e535 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -77,8 +77,17 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(owner.Owner, property, value); - var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); - owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); + if (value.HasValue) + { + var effectiveValue = value.Value; + if (property.ValidateValue?.Invoke(effectiveValue) == false) + effectiveValue = instance.GetCachedDefaultValue(); + owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); + } + else + { + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); + } } if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) From 467ef300bf559123f3dc355185ad78efdf0675ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Feb 2023 10:43:26 +0100 Subject: [PATCH 006/108] Remove unneeded method. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index f36a96992b..a758360545 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -380,23 +380,6 @@ namespace Avalonia.PropertyStore } } - /// - /// Called by non-LocalValue binding entries to re-evaluate the effective value when the - /// binding produces an unset value. - /// - /// The bound property. - /// The priority of binding which produced a new value. - public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority) - { - Debug.Assert(priority != BindingPriority.LocalValue); - - if (TryGetEffectiveValue(property, out var existing)) - { - if (priority <= existing.Priority) - ReevaluateEffectiveValue(property, existing); - } - } - /// /// Called by a when its /// state changes. From 571b7746eac6cc694aa586ec83d48ad5c9f4b9ac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 22 Jan 2023 11:46:28 +0100 Subject: [PATCH 007/108] Added failing integration test. --- samples/IntegrationTestApp/MainWindow.axaml | 1 + .../IntegrationTestApp/MainWindow.axaml.cs | 2 ++ .../WindowTests_MacOS.cs | 24 ++++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b116e4c789..7b1f0eddce 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -139,6 +139,7 @@ Maximized FullScreen + Can Resize diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 3cd5350cce..087f25666b 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -66,11 +66,13 @@ namespace IntegrationTestApp var locationComboBox = this.GetControl("ShowWindowLocation"); var stateComboBox = this.GetControl("ShowWindowState"); var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null; + var canResizeCheckBox = this.GetControl("ShowWindowCanResize"); var owner = (Window)this.GetVisualRoot()!; var window = new ShowWindowTest { WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, + CanResize = canResizeCheckBox.IsChecked.Value, }; if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index d9817ecdd1..06180be74a 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -323,11 +323,30 @@ namespace Avalonia.IntegrationTests.Appium secondaryWindow.GetChromeButtons().close.Click(); } - private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) + [PlatformTheory(TestPlatforms.MacOS)] + [InlineData(ShowWindowMode.NonOwned)] + [InlineData(ShowWindowMode.Owned)] + [InlineData(ShowWindowMode.Modal)] + public void Window_Has_Disabled_Zoom_Button_When_CanResize_Is_False(ShowWindowMode mode) + { + using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false)) + { + var secondaryWindow = GetWindow("SecondaryWindow"); + var (_, _, zoomButton) = secondaryWindow.GetChromeButtons(); + Assert.False(zoomButton.Enabled); + } + } + + private IDisposable OpenWindow( + PixelSize? size, + ShowWindowMode mode, + WindowStartupLocation location, + bool canResize = true) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize"); var showButton = _session.FindElementByAccessibilityId("ShowWindow"); if (size.HasValue) @@ -338,6 +357,9 @@ namespace Avalonia.IntegrationTests.Appium locationComboBox.Click(); _session.FindElementByName(location.ToString()).SendClick(); + + if (canResizeCheckBox.GetIsChecked() != canResize) + canResizeCheckBox.Click(); return showButton.OpenWindowWithClick(); } From 2951b80c39601a8368cc902f76ef4f01bc9307c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Feb 2023 21:26:19 +0100 Subject: [PATCH 008/108] Add additional failing integration tests. For problems introduced in #10153. --- .../WindowTests.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 7bb991aae6..23381c2e58 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -30,9 +30,9 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(StartupLocationData))] - public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location) + public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location, bool canResize) { - using var window = OpenWindow(size, mode, location); + using var window = OpenWindow(size, mode, location, canResize: canResize); var info = GetWindowInfo(); if (size.HasValue) @@ -230,10 +230,10 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(new Rgba32(255, 0, 0), centerColor); } - public static TheoryData StartupLocationData() + public static TheoryData StartupLocationData() { var sizes = new Size?[] { null, new Size(400, 300) }; - var data = new TheoryData(); + var data = new TheoryData(); foreach (var size in sizes) { @@ -243,7 +243,8 @@ namespace Avalonia.IntegrationTests.Appium { if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned)) { - data.Add(size, mode, location); + data.Add(size, mode, location, true); + data.Add(size, mode, location, false); } } } @@ -311,14 +312,16 @@ namespace Avalonia.IntegrationTests.Appium Size? size, ShowWindowMode mode, WindowStartupLocation location = WindowStartupLocation.Manual, - WindowState state = Controls.WindowState.Normal) + WindowState state = Controls.WindowState.Normal, + bool canResize = true) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); var stateComboBox = _session.FindElementByAccessibilityId("ShowWindowState"); + var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize"); var showButton = _session.FindElementByAccessibilityId("ShowWindow"); - + if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); @@ -331,6 +334,9 @@ namespace Avalonia.IntegrationTests.Appium stateComboBox.Click(); _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + if (canResizeCheckBox.GetIsChecked() != canResize) + canResizeCheckBox.Click(); + return showButton.OpenWindowWithClick(); } From f41749170277d8e79c7a5e16af1a1db083705008 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Feb 2023 22:44:24 +0100 Subject: [PATCH 009/108] Speed up integration tests. Don't change combo boxes that are already set to the correct value. --- .../WindowTests.cs | 21 +++++++++++++------ .../WindowTests_MacOS.cs | 14 +++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 23381c2e58..f3861ca267 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -325,14 +325,23 @@ namespace Avalonia.IntegrationTests.Appium if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + if (modeComboBox.GetComboBoxValue() != mode.ToString()) + { + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + } - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + if (locationComboBox.GetComboBoxValue() != location.ToString()) + { + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + } - stateComboBox.Click(); - _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + if (stateComboBox.GetComboBoxValue() != state.ToString()) + { + stateComboBox.Click(); + _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + } if (canResizeCheckBox.GetIsChecked() != canResize) canResizeCheckBox.Click(); diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 06180be74a..0839cbf183 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -352,11 +352,17 @@ namespace Avalonia.IntegrationTests.Appium if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + if (modeComboBox.GetComboBoxValue() != mode.ToString()) + { + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + } - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + if (locationComboBox.GetComboBoxValue() != location.ToString()) + { + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + } if (canResizeCheckBox.GetIsChecked() != canResize) canResizeCheckBox.Click(); From e7a6d6fbc690b96af757199c98ff1f324f3d75ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 12:00:48 +0100 Subject: [PATCH 010/108] Implement windowWillUseStandardFrame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seems that `NSWindow.isZoomed` returns the wrong result unless you implement this? ¯\_(ツ)_/¯ --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index d3b7b4ede6..784072221d 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -223,6 +223,19 @@ } } +// From chromium: +// +// > The delegate or the window class should implement this method so that +// > -[NSWindow isZoomed] can be then determined by whether or not the current +// > window frame is equal to the zoomed frame. +// +// If we don't implement this, then isZoomed always returns true for a non- +// resizable window ¯\_(ツ)_/¯ +- (NSRect)windowWillUseStandardFrame:(NSWindow*)window + defaultFrame:(NSRect)newFrame { + return newFrame; +} + -(BOOL)canBecomeKeyWindow { if(_canBecomeKeyWindow) From 044a499db331b0d13e33d6427643bc1868470d30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 14:27:09 +0100 Subject: [PATCH 011/108] Added additional failing integration tests. --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index f3861ca267..ec24caa18c 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -63,9 +63,9 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(WindowStateData))] - public void WindowState(Size? size, ShowWindowMode mode, WindowState state) + public void WindowState(Size? size, ShowWindowMode mode, WindowState state, bool canResize) { - using var window = OpenWindow(size, mode, state: state); + using var window = OpenWindow(size, mode, state: state, canResize: canResize); try { @@ -253,10 +253,10 @@ namespace Avalonia.IntegrationTests.Appium return data; } - public static TheoryData WindowStateData() + public static TheoryData WindowStateData() { var sizes = new Size?[] { null, new Size(400, 300) }; - var data = new TheoryData(); + var data = new TheoryData(); foreach (var size in sizes) { @@ -274,7 +274,8 @@ namespace Avalonia.IntegrationTests.Appium mode != ShowWindowMode.NonOwned) continue; - data.Add(size, mode, state); + data.Add(size, mode, state, true); + data.Add(size, mode, state, false); } } } From 38aaadf92db4e4957a3ccab2365937c215e3c89a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 15:16:59 +0100 Subject: [PATCH 012/108] Use custom zoom logic when !_canResize. `[NSWindow setIsZoomed]` requires that the window is resizable by the user in order to work; when `canResize == false` this is not that case. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ce82f7d83f..af4f92524b 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -225,16 +225,12 @@ bool WindowImpl::IsZoomed() { } void WindowImpl::DoZoom() { - switch (_decorations) { - case SystemDecorationsNone: - case SystemDecorationsBorderOnly: - [Window setFrame:[Window screen].visibleFrame display:true]; - break; - - - case SystemDecorationsFull: - [Window performZoom:Window]; - break; + if (_decorations == SystemDecorationsNone || + _decorations == SystemDecorationsBorderOnly || + _canResize == false) { + [Window setFrame:[Window screen].visibleFrame display:true]; + } else { + [Window performZoom:Window]; } } From 5346344d12fa314c7317f90f03c8e798ee4ef6cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 17:32:55 +0100 Subject: [PATCH 013/108] Don't call virtual method from ctor. And remove unneeded checks for already-existing `Window` (this method is always called from ctor). --- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 038e9a048c..4d3768a4a8 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -580,21 +580,12 @@ void WindowBaseImpl::CleanNSWindow() { } } -void WindowBaseImpl::CreateNSWindow(bool isDialog) { - if (isDialog) { - if (![Window isKindOfClass:[AvnPanel class]]) { - CleanNSWindow(); - - Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - - [Window setHidesOnDeactivate:false]; - } +void WindowBaseImpl::CreateNSWindow(bool usePanel) { + if (usePanel) { + Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; + [Window setHidesOnDeactivate:false]; } else { - if (![Window isKindOfClass:[AvnWindow class]]) { - CleanNSWindow(); - - Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - } + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; } } From b609ba58a8e445ff68ddbcd64d9ab3d9d4f6cb6c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 17:43:02 +0100 Subject: [PATCH 014/108] Disable zoom button when CanResize == false. To do this more easily, merged `HideOrShowTrafficLights` into a virtual `UpdateStyle`. They were always called together, and really _must_ be called together; this enforces that. --- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 3 +- native/Avalonia.Native/src/OSX/WindowImpl.h | 3 +- native/Avalonia.Native/src/OSX/WindowImpl.mm | 37 +++++++++---------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 4c2758f6c6..afa3a2956f 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -106,8 +106,7 @@ BEGIN_INTERFACE_MAP() protected: virtual NSWindowStyleMask GetStyle(); - - void UpdateStyle(); + virtual void UpdateStyle(); private: void CreateNSWindow (bool isDialog); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 3861aaf170..e7e9b7f1d8 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -41,8 +41,6 @@ BEGIN_INTERFACE_MAP() WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl); - void HideOrShowTrafficLights (); - virtual HRESULT Show (bool activate, bool isDialog) override; virtual HRESULT SetEnabled (bool enable) override; @@ -101,6 +99,7 @@ BEGIN_INTERFACE_MAP() protected: virtual NSWindowStyleMask GetStyle() override; + void UpdateStyle () override; private: void OnInitialiseNSWindow(); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index af4f92524b..ae3e1eaf1d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -30,19 +30,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase OnInitialiseNSWindow(); } -void WindowImpl::HideOrShowTrafficLights() { - if (Window == nil) { - return; - } - - bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; - - [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; -} - void WindowImpl::OnInitialiseNSWindow(){ [GetWindowProtocol() setCanBecomeKeyWindow:true]; @@ -67,8 +54,6 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { WindowBaseImpl::Show(activate, isDialog); GetWindowState(&_actualWindowState); - HideOrShowTrafficLights(); - return SetWindowState(_lastWindowState); } } @@ -257,8 +242,6 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { UpdateStyle(); - HideOrShowTrafficLights(); - switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; @@ -415,9 +398,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } [GetWindowProtocol() setIsExtended:enable]; - - HideOrShowTrafficLights(); - UpdateStyle(); } @@ -608,3 +588,20 @@ NSWindowStyleMask WindowImpl::GetStyle() { } return s; } + +void WindowImpl::UpdateStyle() { + WindowBaseImpl::UpdateStyle(); + + if (Window == nil) { + return; + } + + bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; + + [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowZoomButton] setEnabled:_canResize]; +} + From d40041f02dd8ae5e53074e3b4c3e47b420ff9457 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 19:13:59 +0100 Subject: [PATCH 015/108] GetStyle => abstract CalculateStyleMask. - Make naming more clear - it's not getting the style mask, it's calculating what it should be in order to update the mask - Make it abstract to prevent it being called from the ctor in future --- native/Avalonia.Native/src/OSX/PopupImpl.mm | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 10 +--------- native/Avalonia.Native/src/OSX/WindowImpl.h | 2 +- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm index 9820a9f052..972d03d08c 100644 --- a/native/Avalonia.Native/src/OSX/PopupImpl.mm +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -29,7 +29,7 @@ private: [Window setLevel:NSPopUpMenuWindowLevel]; } protected: - virtual NSWindowStyleMask GetStyle() override + virtual NSWindowStyleMask CalculateStyleMask() override { return NSWindowStyleMaskBorderless; } diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index afa3a2956f..93decef136 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -105,7 +105,7 @@ BEGIN_INTERFACE_MAP() virtual void BringToFront (); protected: - virtual NSWindowStyleMask GetStyle(); + virtual NSWindowStyleMask CalculateStyleMask() = 0; virtual void UpdateStyle(); private: diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 4d3768a4a8..59102e15a6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -35,18 +35,14 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, lastSize = NSSize { 100, 100 }; lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMinSize = NSSize { 0, 0 }; - lastMenu = nullptr; CreateNSWindow(usePanel); [Window setContentView:StandardContainer]; - [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - [Window setContentMinSize:lastMinSize]; [Window setContentMaxSize:lastMaxSize]; - [Window setOpaque:false]; } @@ -564,12 +560,8 @@ bool WindowBaseImpl::IsModal() { return false; } -NSWindowStyleMask WindowBaseImpl::GetStyle() { - return NSWindowStyleMaskBorderless; -} - void WindowBaseImpl::UpdateStyle() { - [Window setStyleMask:GetStyle()]; + [Window setStyleMask:CalculateStyleMask()]; } void WindowBaseImpl::CleanNSWindow() { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index e7e9b7f1d8..9c684c77c4 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -98,7 +98,7 @@ BEGIN_INTERFACE_MAP() bool CanBecomeKeyWindow (); protected: - virtual NSWindowStyleMask GetStyle() override; + virtual NSWindowStyleMask CalculateStyleMask() override; void UpdateStyle () override; private: diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ae3e1eaf1d..afd9c1a5ea 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -553,7 +553,7 @@ bool WindowImpl::IsOwned() { return _parent != nullptr; } -NSWindowStyleMask WindowImpl::GetStyle() { +NSWindowStyleMask WindowImpl::CalculateStyleMask() { unsigned long s = NSWindowStyleMaskBorderless; if(_actualWindowState == FullScreen) From 959b09c2434a285cead448fa8f22a6d282afb90d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 00:18:15 +0100 Subject: [PATCH 016/108] Skip flaky test for now. --- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 0839cbf183..1933d10919 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Normal", windowState.Text); } - [PlatformFact(TestPlatforms.MacOS)] + [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() { // Issue #9565 From 6a1910172c2a060f7ef67d875af03064db768d5f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 10:26:45 +0100 Subject: [PATCH 017/108] Move traffic lights logic into one place. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 4 ---- native/Avalonia.Native/src/OSX/WindowImpl.mm | 15 ++++++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 784072221d..23abf1d53f 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -274,10 +274,6 @@ -(void) setEnabled:(bool)enable { _isEnabled = enable; - - [[self standardWindowButton:NSWindowCloseButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowMiniaturizeButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowZoomButton] setEnabled:enable]; } -(void)becomeKeyWindow diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index afd9c1a5ea..e4bbe24cb8 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -599,9 +599,14 @@ void WindowImpl::UpdateStyle() { bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; - [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setEnabled:_canResize]; -} + NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; + NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; + NSButton* zoomButton = [Window standardWindowButton:NSWindowZoomButton]; + [closeButton setHidden:!hasTrafficLights]; + [closeButton setEnabled:_isEnabled]; + [miniaturizeButton setHidden:!hasTrafficLights]; + [miniaturizeButton setEnabled:_isEnabled]; + [zoomButton setHidden:!hasTrafficLights]; + [zoomButton setEnabled:_isEnabled && _canResize]; +} From a705f546bba1f3fb973aae2cdeb9ed8d08cf4f16 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 11:46:18 +0100 Subject: [PATCH 018/108] Enable resizing during fullscreen transition. macOS seems to tie resizing of the NSView inside the window to the resizable style mask of the window somehow. If we programmatically transition a non-resizable window to fullscreen, the inner NSView's size isn't changed, so we need to make the window resizable during the fullscreen transition. Makes the final two failing `WindowState` integration tests pass. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index e4bbe24cb8..47e83f8d56 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -146,11 +146,13 @@ bool WindowImpl::CanBecomeKeyWindow() void WindowImpl::StartStateTransition() { _transitioningWindowState = true; + UpdateStyle(); } void WindowImpl::EndStateTransition() { _transitioningWindowState = false; - + UpdateStyle(); + // Ensure correct order of child windows after fullscreen transition. BringToFront(); } @@ -573,7 +575,7 @@ NSWindowStyleMask WindowImpl::CalculateStyleMask() { case SystemDecorationsFull: s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable; - if (_canResize && _isEnabled) { + if ((_canResize && _isEnabled) || _transitioningWindowState) { s = s | NSWindowStyleMaskResizable; } break; From 50a368eaa15e7a5756eeda2ec2fdf69d43e9f6da Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 13:05:05 +0100 Subject: [PATCH 019/108] Skip flaky test for now. --- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 1933d10919..47a471fafd 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -56,7 +56,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(TestPlatforms.MacOS)] + [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { var mainWindow = GetWindow("MainWindow"); From 784c380c604b6fd6a139abd7233aaf668661bfd0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 16:10:31 +0100 Subject: [PATCH 020/108] Don't change z-order of window exiting fullscreen. When a window exits fullscreen, its child windows need to be ordered, but we shouldn't touch the z-order of the window itself as this sometimes seemed to result in the parent window being shown over the child windows. Fixes flaky integration tests (hopefully). --- native/Avalonia.Native/src/OSX/WindowImpl.h | 1 + native/Avalonia.Native/src/OSX/WindowImpl.mm | 21 ++++++++++++------- .../WindowTests_MacOS.cs | 4 ++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 9c684c77c4..29bb659039 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -102,6 +102,7 @@ protected: void UpdateStyle () override; private: + void ZOrderChildWindows(); void OnInitialiseNSWindow(); NSString *_lastTitle; }; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 47e83f8d56..4510d135dc 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -119,14 +119,19 @@ void WindowImpl::BringToFront() } [Window invalidateShadow]; + ZOrderChildWindows(); + } +} + +void WindowImpl::ZOrderChildWindows() +{ + for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) + { + auto window = (*iterator)->Window; - for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) - { - auto window = (*iterator)->Window; - - // #9565: Only bring window to front if it's on the currently active space - if ([window isOnActiveSpace]) - (*iterator)->BringToFront(); + // #9565: Only bring window to front if it's on the currently active space + if ([window isOnActiveSpace]) { + (*iterator)->BringToFront(); } } } @@ -154,7 +159,7 @@ void WindowImpl::EndStateTransition() { UpdateStyle(); // Ensure correct order of child windows after fullscreen transition. - BringToFront(); + ZOrderChildWindows(); } SystemDecorations WindowImpl::Decorations() { diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 47a471fafd..0839cbf183 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -56,7 +56,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] + [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { var mainWindow = GetWindow("MainWindow"); @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Normal", windowState.Text); } - [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] + [PlatformFact(TestPlatforms.MacOS)] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() { // Issue #9565 From 1abb1abaf5f03ee87f66946acc3b2fc709a45d73 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Feb 2023 10:42:17 +0100 Subject: [PATCH 021/108] Don't overwrite unrelated style mask bits. Fixes a problem where we were clearing the fullscreen flag erroneously during a fullscreen transition, and in general it's best to preserve flags we're not interested in controlling anyway. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 4510d135dc..cf1ee6943d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -561,13 +561,15 @@ bool WindowImpl::IsOwned() { } NSWindowStyleMask WindowImpl::CalculateStyleMask() { - unsigned long s = NSWindowStyleMaskBorderless; + // Use the current style mask and only clear the flags we're going to be modifying. + unsigned long s = [Window styleMask] & + ~(NSWindowStyleMaskFullSizeContentView | + NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskResizable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskTexturedBackground); - if(_actualWindowState == FullScreen) - { - s |= NSWindowStyleMaskFullScreen; - } - switch (_decorations) { case SystemDecorationsNone: s = s | NSWindowStyleMaskFullSizeContentView; From 339b1e92bdc95c8b07b6c7f410744719883ffafe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Feb 2023 10:46:04 +0100 Subject: [PATCH 022/108] Add hack for strange win32 behavior. Fixes two failing integration tests on win32. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 1e0d92d442..22b43b1c18 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1180,8 +1180,21 @@ namespace Avalonia.Win32 var y = monitorInfo.rcWork.top; var cx = Math.Abs(monitorInfo.rcWork.right - x); var cy = Math.Abs(monitorInfo.rcWork.bottom - y); + var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); - SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); + if (!style.HasFlag(WindowStyles.WS_SIZEFRAME)) + { + // When calling SetWindowPos on a maximized window it automatically adjusts + // for "hidden" borders which are placed offscreen, EVEN IF THE WINDOW HAS + // NO BORDERS, meaning that the window is placed wrong when we have CanResize + // == false. Account for this here. + var borderThickness = BorderThickness; + x -= (int)borderThickness.Left; + cx += (int)borderThickness.Left + (int)borderThickness.Right; + cy += (int)borderThickness.Bottom; + } + + SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_FRAMECHANGED); } } } From 008cfce9a87cb64f33982fb90b10eb1e58ebaa1e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Feb 2023 10:46:12 +0100 Subject: [PATCH 023/108] Update ncrunch config. --- .ncrunch/Avalonia.UnitTests.v3.ncrunchproject | 5 +++++ .ncrunch/GpuInterop.v3.ncrunchproject | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .ncrunch/Avalonia.UnitTests.v3.ncrunchproject create mode 100644 .ncrunch/GpuInterop.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..cff5044edf --- /dev/null +++ b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/GpuInterop.v3.ncrunchproject b/.ncrunch/GpuInterop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/GpuInterop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From a04af7b780867d730bdbdcf9cbaf0c54117ce1bb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 11:23:23 +0100 Subject: [PATCH 024/108] Update ncrunch config. --- .ncrunch/Avalonia.UnitTests.v3.ncrunchproject | 5 +++++ .ncrunch/GpuInterop.v3.ncrunchproject | 5 +++++ Avalonia.Desktop.slnf | 1 + 3 files changed, 11 insertions(+) create mode 100644 .ncrunch/Avalonia.UnitTests.v3.ncrunchproject create mode 100644 .ncrunch/GpuInterop.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..cff5044edf --- /dev/null +++ b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/GpuInterop.v3.ncrunchproject b/.ncrunch/GpuInterop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/GpuInterop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 3acd4bf9f2..741570061b 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -45,6 +45,7 @@ "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", + "tests\\Avalonia.Controls.ItemsRepeater.UnitTests\\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "tests\\Avalonia.Controls.UnitTests\\Avalonia.Controls.UnitTests.csproj", "tests\\Avalonia.DesignerSupport.TestApp\\Avalonia.DesignerSupport.TestApp.csproj", "tests\\Avalonia.DesignerSupport.Tests\\Avalonia.DesignerSupport.Tests.csproj", From 035f4f0e55ba6115d6d064c5cbcb6eed91983ada Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 12:02:18 +0100 Subject: [PATCH 025/108] Added failing integration tests for #10232. --- samples/IntegrationTestApp/MainWindow.axaml | 1 + .../ComboBoxTests.cs | 61 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b116e4c789..4e5a8463df 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -70,6 +70,7 @@ Item 0 Item 1 + Wrap Selection diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index abdb4e2dd8..8df7873582 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -47,7 +47,64 @@ namespace Avalonia.IntegrationTests.Appium } [PlatformFact(TestPlatforms.Windows)] - public void Can_Change_Selection_With_Keyboard() + public void Can_Change_Selection_With_Keyboard_When_Closed() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + var wrap = _session.FindElementByAccessibilityId("ComboBoxWrapSelection"); + + if (wrap.GetIsChecked() != false) + wrap.Click(); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowUp); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowUp); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + } + + [PlatformFact(TestPlatforms.Windows)] + public void Can_Change_Wrapping_Selection_With_Keyboard_When_Closed() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + var wrap = _session.FindElementByAccessibilityId("ComboBoxWrapSelection"); + + if (wrap.GetIsChecked() != true) + wrap.Click(); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowUp); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowUp); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + } + + [PlatformFact(TestPlatforms.Windows)] + public void Can_Change_Selection_When_Open_With_Keyboard() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); @@ -64,7 +121,7 @@ namespace Avalonia.IntegrationTests.Appium } [PlatformFact(TestPlatforms.Windows)] - public void Can_Change_Selection_With_Keyboard_From_Unselected() + public void Can_Change_Selection_When_Open_With_Keyboard_From_Unselected() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); From fdc65d1c4e41effe5913e73e6e90d4edfa5f1551 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 12:04:10 +0100 Subject: [PATCH 026/108] Different selection logic when ComboBox closed. When the `ComboBox` is closed, then the existing `MoveSelection` method doesn't work because the popup containing the `ItemsPresenter` is not visible, and so has no realized items. Instead revert to simple index-based selection logic when the dropdown is closed. Fixes #10232 --- src/Avalonia.Controls/ComboBox.cs | 46 ++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index b7a298bb16..2d810236c3 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,10 +1,7 @@ using System; using System.Linq; using Avalonia.Automation.Peers; -using Avalonia.Reactive; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Mixins; -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; @@ -12,8 +9,8 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.VisualTree; -using Avalonia.Controls.Metadata; namespace Avalonia.Controls { @@ -482,7 +479,22 @@ namespace Avalonia.Controls { if (ItemCount >= 1) { - MoveSelection(NavigationDirection.Next, WrapSelection); + if (IsDropDownOpen) + { + MoveSelection(NavigationDirection.Next, WrapSelection); + } + else + { + var index = SelectedIndex + 1; + var count = ItemCount; + + if (WrapSelection) + index %= count; + else + index = Math.Min(index, count - 1); + + SelectedIndex = index; + } } } @@ -490,7 +502,27 @@ namespace Avalonia.Controls { if (ItemCount >= 1) { - MoveSelection(NavigationDirection.Previous, WrapSelection); + if (IsDropDownOpen) + { + MoveSelection(NavigationDirection.Previous, WrapSelection); + } + else + { + var index = SelectedIndex - 1; + var count = ItemCount; + + if (WrapSelection) + { + if (index < 0) + index += count; + } + else + { + index = Math.Max(index, 0); + } + + SelectedIndex = index; + } } } } From a331e1f86b843e37b54e9a3a7377170040124d04 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 18:40:03 +0100 Subject: [PATCH 027/108] Account for enabled state of combo box items. --- src/Avalonia.Controls/ComboBox.cs | 65 +++++++++++++------------------ 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 2d810236c3..17a6ad7a09 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,10 +1,13 @@ using System; +using System.Diagnostics; using System.Linq; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; @@ -216,7 +219,7 @@ namespace Avalonia.Controls } else if (e.Key == Key.Up) { - SelectPrev(); + SelectPrevious(); e.Handled = true; } } @@ -247,7 +250,7 @@ namespace Avalonia.Controls if (e.Delta.Y < 0) SelectNext(); else - SelectPrev(); + SelectPrevious(); e.Handled = true; } @@ -475,53 +478,39 @@ namespace Avalonia.Controls } } - private void SelectNext() + private void SelectNext() => MoveSelection(SelectedIndex, 1, WrapSelection); + private void SelectPrevious() => MoveSelection(SelectedIndex, -1, WrapSelection); + + private void MoveSelection(int startIndex, int step, bool wrap) { - if (ItemCount >= 1) - { - if (IsDropDownOpen) - { - MoveSelection(NavigationDirection.Next, WrapSelection); - } - else - { - var index = SelectedIndex + 1; - var count = ItemCount; + static bool IsSelectable(object? o) => (o as AvaloniaObject)?.GetValue(IsEnabledProperty) ?? true; - if (WrapSelection) - index %= count; - else - index = Math.Min(index, count - 1); + var count = ItemCount; - SelectedIndex = index; - } - } - } - - private void SelectPrev() - { - if (ItemCount >= 1) + for (int i = startIndex + step; i != startIndex; i += step) { - if (IsDropDownOpen) - { - MoveSelection(NavigationDirection.Previous, WrapSelection); - } - else + if (i < 0 || i >= count) { - var index = SelectedIndex - 1; - var count = ItemCount; - - if (WrapSelection) + if (wrap) { - if (index < 0) - index += count; + if (i < 0) + i += count; + else if (i >= count) + i %= count; } else { - index = Math.Max(index, 0); + return; } + } - SelectedIndex = index; + var item = ItemsView[i]; + var container = ContainerFromIndex(i); + + if (IsSelectable(item) && IsSelectable(container)) + { + SelectedIndex = i; + break; } } } From 4b0ff3be639755567691c0964f39ee37796ac9f7 Mon Sep 17 00:00:00 2001 From: DJGosnell Date: Thu, 9 Feb 2023 15:05:46 -0500 Subject: [PATCH 028/108] Initial work for adding caches for SKTextBlobBuilder, SKRoundRect, SKFont usages. Updates SKPaintCache for new caching base class. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 43 +++++++---- src/Skia/Avalonia.Skia/GeometryImpl.cs | 4 +- .../Gpu/OpenGl/GlRenderTarget.cs | 4 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 47 ++++++------ src/Skia/Avalonia.Skia/SKCacheBase.cs | 72 +++++++++++++++++++ src/Skia/Avalonia.Skia/SKFontCache.cs | 13 ++++ src/Skia/Avalonia.Skia/SKPaintCache.cs | 58 ++------------- src/Skia/Avalonia.Skia/SKRoundRectCache.cs | 26 +++++++ .../Avalonia.Skia/SKTextBlobBuilderCache.cs | 13 ++++ src/Skia/Avalonia.Skia/TextShaperImpl.cs | 6 +- 10 files changed, 194 insertions(+), 92 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/SKCacheBase.cs create mode 100644 src/Skia/Avalonia.Skia/SKFontCache.cs create mode 100644 src/Skia/Avalonia.Skia/SKRoundRectCache.cs create mode 100644 src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ba646c64ee..a29cbb1cc3 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -34,9 +34,9 @@ namespace Avalonia.Skia private GRContext _grContext; public GRContext GrContext => _grContext; private ISkiaGpu _gpu; - private readonly SKPaint _strokePaint = SKPaintCache.Get(); - private readonly SKPaint _fillPaint = SKPaintCache.Get(); - private readonly SKPaint _boxShadowPaint = SKPaintCache.Get(); + private readonly SKPaint _strokePaint = SKPaintCache.Shared.Get(); + private readonly SKPaint _fillPaint = SKPaintCache.Shared.Get(); + private readonly SKPaint _boxShadowPaint = SKPaintCache.Shared.Get(); private static SKShader s_acrylicNoiseShader; private readonly ISkiaGpuRenderSession _session; private bool _leased = false; @@ -186,13 +186,13 @@ namespace Avalonia.Skia var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); - var paint = SKPaintCache.Get(); + var paint = SKPaintCache.Shared.Get(); paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = _currentBlendingMode.ToSKBlendMode(); drawableImage.Draw(this, s, d, paint); - SKPaintCache.ReturnReset(paint); + SKPaintCache.Shared.ReturnReset(paint); } /// @@ -535,7 +535,24 @@ namespace Avalonia.Skia { CheckLease(); Canvas.Save(); - Canvas.ClipRoundRect(clip.ToSKRoundRect(), antialias:true); + + // Get the rounded rectangle + var rc = clip.Rect.ToSKRect(); + + // Get a round rect from the cache. + var roundRect = SKRoundRectCache.Shared.Get(); + + roundRect.SetRectRadii(rc, + new[] + { + clip.RadiiTopLeft.ToSKPoint(), clip.RadiiTopRight.ToSKPoint(), + clip.RadiiBottomRight.ToSKPoint(), clip.RadiiBottomLeft.ToSKPoint(), + }); + + Canvas.ClipRoundRect(roundRect, antialias:true); + + // Should not need to reset as SetRectRadii overrides the values. + SKRoundRectCache.Shared.Return(roundRect); } /// @@ -569,9 +586,9 @@ namespace Avalonia.Skia try { // Return leased paints. - SKPaintCache.ReturnReset(_strokePaint); - SKPaintCache.ReturnReset(_fillPaint); - SKPaintCache.ReturnReset(_boxShadowPaint); + SKPaintCache.Shared.ReturnReset(_strokePaint); + SKPaintCache.Shared.ReturnReset(_fillPaint); + SKPaintCache.Shared.ReturnReset(_boxShadowPaint); if (_grContext != null) { @@ -633,7 +650,7 @@ namespace Avalonia.Skia { CheckLease(); - var paint = SKPaintCache.Get(); + var paint = SKPaintCache.Shared.Get(); Canvas.SaveLayer(paint); _maskStack.Push(CreatePaint(paint, mask, bounds.Size)); @@ -644,11 +661,11 @@ namespace Avalonia.Skia { CheckLease(); - var paint = SKPaintCache.Get(); + var paint = SKPaintCache.Shared.Get(); paint.BlendMode = SKBlendMode.DstIn; Canvas.SaveLayer(paint); - SKPaintCache.ReturnReset(paint); + SKPaintCache.Shared.ReturnReset(paint); PaintWrapper paintWrapper; using (paintWrapper = _maskStack.Pop()) @@ -656,7 +673,7 @@ namespace Avalonia.Skia Canvas.DrawPaint(paintWrapper.Paint); } // Return the paint wrapper's paint less the reset since the paint is already reset in the Dispose method above. - SKPaintCache.Return(paintWrapper.Paint); + SKPaintCache.Shared.Return(paintWrapper.Paint); Canvas.Restore(); diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 15a3ebff40..51386d2a45 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -81,12 +81,12 @@ namespace Avalonia.Skia } else { - var paint = SKPaintCache.Get(); + var paint = SKPaintCache.Shared.Get(); paint.IsStroke = true; paint.StrokeWidth = strokeWidth; paint.GetFillPath(EffectivePath, strokePath); - SKPaintCache.ReturnReset(paint); + SKPaintCache.Shared.ReturnReset(paint); _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect()); } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 4b3c7a016d..25e004f4ef 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -13,7 +13,7 @@ namespace Avalonia.Skia { private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; - + private static readonly SKSurfaceProperties _surfaceProperties = new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal); public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurface glSurface) { _grContext = grContext; @@ -92,7 +92,7 @@ namespace Avalonia.Skia var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, samples, disp.StencilSize, glInfo); var surface = SKSurface.Create(_grContext, renderTarget, glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, - colorType, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); + colorType, _surfaceProperties); success = true; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index e795f3d304..8e9a19239b 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -76,13 +76,14 @@ namespace Avalonia.Skia } var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize; - var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize) - { - Size = fontRenderingEmSize, - Edging = SKFontEdging.Alias, - Hinting = SKFontHinting.None, - LinearMetrics = true - }; + + var skFont = SKFontCache.Shared.Get(); + + skFont.Typeface = glyphTypeface.Typeface; + skFont.Size = fontRenderingEmSize; + skFont.Edging = SKFontEdging.Alias; + skFont.Hinting = SKFontHinting.None; + skFont.LinearMetrics = true; SKPath path = new SKPath(); @@ -101,6 +102,8 @@ namespace Avalonia.Skia currentX += glyphRun.GlyphInfos[i].GlyphAdvance; } + SKFontCache.Shared.Return(skFont); + return new StreamGeometryImpl(path); } @@ -224,20 +227,19 @@ namespace Avalonia.Skia var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; - var font = new SKFont - { - LinearMetrics = true, - Subpixel = true, - Edging = SKFontEdging.SubpixelAntialias, - Hinting = SKFontHinting.Full, - Size = (float)fontRenderingEmSize, - Typeface = glyphTypefaceImpl.Typeface, - Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0, - SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0 - }; + var font = SKFontCache.Shared.Get(); + + font.LinearMetrics = true; + font.Subpixel = true; + font.Edging = SKFontEdging.SubpixelAntialias; + font.Hinting = SKFontHinting.Full; + font.Size = (float)fontRenderingEmSize; + font.Typeface = glyphTypefaceImpl.Typeface; + font.Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0; + font.SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0; - var builder = new SKTextBlobBuilder(); + var builder = SKTextBlobBuilderCache.Shared.Get(); var count = glyphInfos.Count; var runBuffer = builder.AllocatePositionedRun(font, count); @@ -245,6 +247,8 @@ namespace Avalonia.Skia var glyphSpan = runBuffer.GetGlyphSpan(); var positionSpan = runBuffer.GetPositionSpan(); + SKFontCache.Shared.Return(font); + var width = 0.0; for (int i = 0; i < count; i++) @@ -261,8 +265,11 @@ namespace Avalonia.Skia var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; var height = glyphTypeface.Metrics.LineSpacing * scale; + var skTextBlob = builder.Build(); + + SKTextBlobBuilderCache.Shared.Return(builder); - return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin); + return new GlyphRunImpl(skTextBlob, new Size(width, height), baselineOrigin); } } } diff --git a/src/Skia/Avalonia.Skia/SKCacheBase.cs b/src/Skia/Avalonia.Skia/SKCacheBase.cs new file mode 100644 index 0000000000..e1e78cd081 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKCacheBase.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Concurrent; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Cache base for Skia objects. + /// + internal abstract class SKCacheBase + where TCachedItem : IDisposable, new() + where TCache : new() + { + /// + /// Bag to hold the cached items. + /// + protected readonly ConcurrentBag Cache; + + /// + /// Shared cache. + /// + public static readonly TCache Shared = new TCache(); + + protected SKCacheBase() + { + Cache = new ConcurrentBag(); + } + + /// + /// Gets a cached item for usage. + /// + /// + /// If there is a available item in the cache, the cached item will be returned.. + /// Otherwise a new cached item will be created. + /// + /// + public TCachedItem Get() + { + if (!Cache.TryTake(out var item)) + { + item = new TCachedItem(); + } + + return item; + } + + /// + /// Returns the item for reuse later. + /// + /// + /// Do not use the item further. + /// Do not return the same item multiple times as that will break the cache. + /// + /// + public void Return(TCachedItem item) + { + Cache.Add(item); + } + + /// + /// Clears and disposes all cached items. + /// + public void Clear() + { + while (Cache.TryTake(out var item)) + { + item.Dispose(); + } + } + + } +} diff --git a/src/Skia/Avalonia.Skia/SKFontCache.cs b/src/Skia/Avalonia.Skia/SKFontCache.cs new file mode 100644 index 0000000000..348e085253 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKFontCache.cs @@ -0,0 +1,13 @@ +using System.Collections.Concurrent; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Cache for SKFonts. + /// + internal class SKFontCache : SKCacheBase + { + + } +} diff --git a/src/Skia/Avalonia.Skia/SKPaintCache.cs b/src/Skia/Avalonia.Skia/SKPaintCache.cs index 6588ab8da8..82c4dd23c7 100644 --- a/src/Skia/Avalonia.Skia/SKPaintCache.cs +++ b/src/Skia/Avalonia.Skia/SKPaintCache.cs @@ -6,46 +6,8 @@ namespace Avalonia.Skia /// /// Cache for SKPaints. /// - internal static class SKPaintCache + internal class SKPaintCache : SKCacheBase { - private static ConcurrentBag s_cachedPaints; - - static SKPaintCache() - { - s_cachedPaints = new ConcurrentBag(); - } - - /// - /// Gets a SKPaint for usage. - /// - /// - /// If a SKPaint is in the cache, that existing SKPaint will be returned. - /// Otherwise a new SKPaint will be created. - /// - /// - public static SKPaint Get() - { - if (!s_cachedPaints.TryTake(out var paint)) - { - paint = new SKPaint(); - } - - return paint; - } - - /// - /// Returns a SKPaint for reuse later. - /// - /// - /// Do not use the paint further. - /// Do not return the same paint multiple times as that will break the cache. - /// - /// - public static void Return(SKPaint paint) - { - s_cachedPaints.Add(paint); - } - /// /// Returns a SKPaint and resets it for reuse later. /// @@ -54,23 +16,11 @@ namespace Avalonia.Skia /// Do not return the same paint multiple times as that will break the cache. /// Uses SKPaint.Reset() for reuse later. /// - /// - public static void ReturnReset(SKPaint paint) + /// Paint to reset. + public void ReturnReset(SKPaint paint) { paint.Reset(); - s_cachedPaints.Add(paint); + Cache.Add(paint); } - - /// - /// Clears and disposes all cached paints. - /// - public static void Clear() - { - while (s_cachedPaints.TryTake(out var paint)) - { - paint.Dispose(); - } - } - } } diff --git a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs new file mode 100644 index 0000000000..e164f97d6a --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs @@ -0,0 +1,26 @@ +using System.Collections.Concurrent; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Cache for SKPaints. + /// + internal class SKRoundRectCache : SKCacheBase + { + /// + /// Returns a SKPaint and resets it for reuse later. + /// + /// + /// Do not use the rect further. + /// Do not return the same rect multiple times as that will break the cache. + /// Uses SKRoundRect.SetEmpty(); for reuse later. + /// + /// Rectangle to reset + public void ReturnReset(SKRoundRect rect) + { + rect.SetEmpty(); + Cache.Add(rect); + } + } +} diff --git a/src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs b/src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs new file mode 100644 index 0000000000..8c010ecb05 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs @@ -0,0 +1,13 @@ +using System.Collections.Concurrent; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Cache for SKTextBlobBuilder. + /// + internal class SKTextBlobBuilderCache : SKCacheBase + { + + } +} diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index e1a6b93692..a21038839c 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; @@ -13,6 +14,7 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { + private static readonly ConcurrentDictionary s_cachedLanguage = new(); public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var textSpan = text.Span; @@ -33,7 +35,9 @@ namespace Avalonia.Skia buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + var usedCulture = culture ?? CultureInfo.CurrentCulture; + + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, i => new Language(usedCulture)); var font = ((GlyphTypefaceImpl)typeface).Font; From 87709b960637a4bf12a170d95dd4f420eab31c09 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 9 Feb 2023 20:44:12 +0000 Subject: [PATCH 029/108] add IsScrollInertiaEnabled setter and getter --- src/Avalonia.Controls/ScrollViewer.cs | 16 ++++++++++++++++ src/Avalonia.Themes.Fluent/Controls/ListBox.xaml | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index ab114da933..af874bd380 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -775,6 +775,22 @@ namespace Avalonia.Controls control.SetValue(VerticalScrollBarVisibilityProperty, value); } + /// + /// Gets whether scroll gestures should include inertia in their behavior and value. + /// + public static bool GetIsScrollInertiaEnabled(Control control) + { + return control.GetValue(IsScrollInertiaEnabledProperty); + } + + /// + /// Sets whether scroll gestures should include inertia in their behavior and value. + /// + public static void SetIsScrollInertiaEnabled(Control control, bool value) + { + control.SetValue(IsScrollInertiaEnabledProperty, value); + } + /// public void RegisterAnchorCandidate(Control element) { diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index 4b9fb76b8a..e2273cd487 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -19,6 +19,7 @@ + @@ -34,6 +35,7 @@ HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" + IsScrollInertiaEnabled="{TemplateBinding (ScrollViewer.IsScrollInertiaEnabled)}" AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> Date: Thu, 9 Feb 2023 16:53:40 -0500 Subject: [PATCH 030/108] Cached round SKRoundRects created with DrawingContextImpl.DrawRectangle --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 34 +++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a29cbb1cc3..2bb6f1dc7e 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -315,15 +315,20 @@ namespace Avalonia.Skia var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; var needRoundRect = rect.IsRounded; - using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + SKRoundRect skRoundRect = null; if (needRoundRect) + { + skRoundRect = SKRoundRectCache.Shared.Get(); skRoundRect.SetRectRadii(rc, new[] { - rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), - rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + rect.RadiiTopLeft.ToSKPoint(), + rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), + rect.RadiiBottomLeft.ToSKPoint(), }); + } if (material != null) { @@ -332,6 +337,7 @@ namespace Avalonia.Skia if (isRounded) { Canvas.DrawRoundRect(skRoundRect, paint.Paint); + SKRoundRectCache.Shared.Return(skRoundRect); } else { @@ -356,14 +362,19 @@ namespace Avalonia.Skia var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); - using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + SKRoundRect skRoundRect = null; if (needRoundRect) + { + skRoundRect = SKRoundRectCache.Shared.Get(); skRoundRect.SetRectRadii(rc, new[] { - rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), - rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + rect.RadiiTopLeft.ToSKPoint(), + rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), + rect.RadiiBottomLeft.ToSKPoint(), }); + } foreach (var boxShadow in boxShadows) { @@ -378,7 +389,8 @@ namespace Avalonia.Skia Canvas.Save(); if (isRounded) { - using var shadowRect = new SKRoundRect(skRoundRect); + var shadowRect = SKRoundRectCache.Shared.Get(); + shadowRect.SetRectRadii(skRoundRect!.Rect, skRoundRect.Radii); if (spread != 0) shadowRect.Inflate(spread, spread); Canvas.ClipRoundRect(skRoundRect, @@ -388,6 +400,7 @@ namespace Avalonia.Skia Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY); Canvas.DrawRoundRect(shadowRect, shadow.Paint); Transform = oldTransform; + SKRoundRectCache.Shared.Return(shadowRect); } else { @@ -433,7 +446,8 @@ namespace Avalonia.Skia var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); Canvas.Save(); - using var shadowRect = new SKRoundRect(skRoundRect); + var shadowRect = SKRoundRectCache.Shared.Get(); + shadowRect.SetRectRadii(skRoundRect!.Rect, skRoundRect.Radii); if (spread != 0) shadowRect.Deflate(spread, spread); Canvas.ClipRoundRect(skRoundRect, @@ -445,6 +459,7 @@ namespace Avalonia.Skia Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint); Transform = oldTransform; Canvas.Restore(); + SKRoundRectCache.Shared.Return(shadowRect); } } } @@ -466,6 +481,9 @@ namespace Avalonia.Skia } } } + + if(isRounded) + SKRoundRectCache.Shared.Return(skRoundRect); } /// From 2c0c03d317c358add598c6406ea4c6d33f97aaee Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 10 Feb 2023 01:16:47 +0100 Subject: [PATCH 031/108] Fix CustomDrawingExampleControl crash on Direct2D --- .../Pages/CustomDrawingExampleControl.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs index 11e5e32cf1..938c45a4e2 100644 --- a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs +++ b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs @@ -59,10 +59,12 @@ namespace ControlCatalog.Pages }; StreamGeometry sg = new StreamGeometry(); - var cntx = sg.Open(); - cntx.BeginFigure(new Point(-25.0d, -10.0d), false); - cntx.ArcTo(new Point(25.0d, -10.0d), new Size(10.0d, 10.0d), 0.0d, false, SweepDirection.Clockwise); - cntx.EndFigure(true); + using (var cntx = sg.Open()) + { + cntx.BeginFigure(new Point(-25.0d, -10.0d), false); + cntx.ArcTo(new Point(25.0d, -10.0d), new Size(10.0d, 10.0d), 0.0d, false, SweepDirection.Clockwise); + cntx.EndFigure(true); + } _smileGeometry = sg.Clone(); } From 3568e60841a4cd2229a1a1b22d8dd1cfa4392d1d Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 10 Feb 2023 01:20:47 +0100 Subject: [PATCH 032/108] Set WindowsInteropTest platform target to x64 --- .../interop/WindowsInteropTest/WindowsInteropTest.csproj | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 1643ca3ee2..95f77f6df9 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -2,7 +2,7 @@ WinExe net461 - + x64 true true @@ -10,9 +10,6 @@ - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - + From 7d879cf6febe500a6f5f7fcda786b88d075abbf0 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 10 Feb 2023 13:00:25 +0100 Subject: [PATCH 033/108] Added Win32PlatformOptions.ShouldRenderOnUIThread Fixes WPF interop --- samples/interop/WindowsInteropTest/Program.cs | 11 +++++++-- .../Rendering/DefaultRenderTimer.cs | 5 ++-- src/Avalonia.Base/Rendering/IRenderLoop.cs | 5 +++- src/Avalonia.Base/Rendering/RenderLoop.cs | 1 + .../Rendering/UiThreadRenderTimer.cs | 9 ++++++- .../Wpf/WpfTopLevelImpl.cs | 9 +------ src/Windows/Avalonia.Win32/Win32Platform.cs | 24 +++++++++++++------ 7 files changed, 42 insertions(+), 22 deletions(-) diff --git a/samples/interop/WindowsInteropTest/Program.cs b/samples/interop/WindowsInteropTest/Program.cs index fac06d74b0..c2d30c67bb 100644 --- a/samples/interop/WindowsInteropTest/Program.cs +++ b/samples/interop/WindowsInteropTest/Program.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Controls; using ControlCatalog; using Avalonia; @@ -15,7 +14,15 @@ namespace WindowsInteropTest { System.Windows.Forms.Application.EnableVisualStyles(); System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); - AppBuilder.Configure().UseWin32().UseDirect2D1().SetupWithoutStarting(); + AppBuilder.Configure() + .UseWin32() + .UseDirect2D1() + .With(new Win32PlatformOptions + { + UseWindowsUIComposition = false, + ShouldRenderOnUIThread = true // necessary for WPF + }) + .SetupWithoutStarting(); System.Windows.Forms.Application.Run(new SelectorForm()); } } diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index d0d3dd9715..7b0fecf675 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -1,6 +1,4 @@ using System; -using System.Diagnostics; -using System.Threading.Tasks; using Avalonia.Platform; namespace Avalonia.Rendering @@ -59,7 +57,8 @@ namespace Avalonia.Rendering } } - public bool RunsInBackground => true; + /// + public virtual bool RunsInBackground => true; /// /// Starts the timer. diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index e500ecdf8b..ebe683949d 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoop.cs @@ -27,7 +27,10 @@ namespace Avalonia.Rendering /// /// The update task. void Remove(IRenderLoopTask i); - + + /// + /// Indicates if the rendering is done on a non-UI thread. + /// bool RunsInBackground { get; } } } diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index 1f58ca3827..185f44d29a 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -87,6 +87,7 @@ namespace Avalonia.Rendering } } + /// public bool RunsInBackground => Timer.RunsInBackground; private void TimerTick(TimeSpan time) diff --git a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs index 1bbf804b5f..7f2eedc98c 100644 --- a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs @@ -8,13 +8,20 @@ namespace Avalonia.Rendering /// /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms /// - public class UiThreadRenderTimer : DefaultRenderTimer { + /// + /// Initializes a new instance of the class. + /// + /// The number of frames per second at which the loop should run. public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond) { } + /// + public override bool RunsInBackground => false; + + /// protected override IDisposable StartCore(Action tick) { bool cancelled = false; diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 2d0f351d58..13eae1992c 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -45,13 +45,6 @@ namespace Avalonia.Win32.Interop.Wpf ((FrameworkElement)PlatformImpl)?.InvalidateMeasure(); } - protected override void HandleResized(Size clientSize, PlatformResizeReason reason) - { - ClientSize = clientSize; - LayoutManager.ExecuteLayoutPass(); - Renderer?.Resized(clientSize); - } - public Size AllocatedSize => ClientSize; } @@ -223,7 +216,7 @@ namespace Avalonia.Win32.Interop.Wpf (Key)e.Key, GetModifiers(null))); - protected override void OnTextInput(TextCompositionEventArgs e) + protected override void OnTextInput(TextCompositionEventArgs e) => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, e.Text)); void ITopLevelImpl.SetCursor(ICursorImpl cursor) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index f16a1ca8cf..c34be9008a 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -6,12 +6,10 @@ using System.IO; using Avalonia.Reactive; using System.Runtime.InteropServices; using System.Threading; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.Media; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; @@ -20,7 +18,6 @@ using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; -using Avalonia.Win32.WinRT; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia @@ -68,6 +65,7 @@ namespace Avalonia /// /// Render Avalonia to a Texture inside the Windows.UI.Composition tree. + /// This setting is true by default. /// /// /// Supported on Windows 10 build 16299 and above. Ignored on other versions. @@ -88,9 +86,19 @@ namespace Avalonia /// This is only recommended if low input latency is desirable, and there is no need for the transparency /// and stylings / blurrings offered by
/// This is mutually exclusive with - /// which if active will override this setting. + /// which if active will override this setting. + /// This setting is false by default. ///
- public bool UseLowLatencyDxgiSwapChain { get; set; } = false; + public bool UseLowLatencyDxgiSwapChain { get; set; } + + /// + /// Render directly on the UI thread instead of using a dedicated render thread. + /// Only applicable if both and + /// are false. + /// This setting is only recommended for interop with systems that must render on the UI thread, such as WPF. + /// This setting is false by default. + /// + public bool ShouldRenderOnUIThread { get; set; } /// /// Provides a way to use a custom-implemented graphics context such as a custom ISkiaGpu @@ -128,7 +136,7 @@ namespace Avalonia.Win32 internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } - + internal static Compositor Compositor { get; private set; } public static void Initialize() @@ -139,6 +147,8 @@ namespace Avalonia.Win32 public static void Initialize(Win32PlatformOptions options) { Options = options; + var renderTimer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) : new DefaultRenderTimer(60); + AvaloniaLocator.CurrentMutable .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) @@ -146,7 +156,7 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(renderTimer) .Bind().ToConstant(s_instance) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control) { From 1c9032dd7228ae2c2ae520dae690f0851fda1a33 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 10 Feb 2023 13:04:14 +0100 Subject: [PATCH 034/108] Win32 tray icon: avoid HIcon access when removing Prevents a crash when the TrayIconImpl finalizer runs after the System.Drawing.Icon has been disposed --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index ab70d77a09..fe920ff1a1 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -81,19 +81,18 @@ namespace Avalonia.Win32 private void UpdateIcon(bool remove = false) { - var iconData = new NOTIFYICONDATA() + var iconData = new NOTIFYICONDATA { hWnd = Win32Platform.Instance.Handle, - uID = _uniqueId, - uFlags = NIF.TIP | NIF.MESSAGE, - uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE, - hIcon = _icon?.HIcon ?? s_emptyIcon, - szTip = _tooltipText ?? "" + uID = _uniqueId }; if (!remove) { - iconData.uFlags |= NIF.ICON; + iconData.uFlags = NIF.TIP | NIF.MESSAGE | NIF.ICON; + iconData.uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE; + iconData.hIcon = _icon?.HIcon ?? s_emptyIcon; + iconData.szTip = _tooltipText ?? ""; if (!_iconAdded) { @@ -107,6 +106,7 @@ namespace Avalonia.Win32 } else { + iconData.uFlags = 0; Shell_NotifyIcon(NIM.DELETE, iconData); _iconAdded = false; } From 23d999c2eb1d5ce39e42d1c8ce36243d7d30d47a Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Fri, 10 Feb 2023 19:18:18 +0200 Subject: [PATCH 035/108] Make all Render overrides sealed (#10299) --- src/Avalonia.Controls/Border.cs | 2 +- src/Avalonia.Controls/ExperimentalAcrylicBorder.cs | 2 +- src/Avalonia.Controls/Grid.cs | 2 +- src/Avalonia.Controls/Image.cs | 2 +- src/Avalonia.Controls/Panel.cs | 2 +- src/Avalonia.Controls/Presenters/ContentPresenter.cs | 2 +- src/Avalonia.Controls/Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/Primitives/AccessText.cs | 5 ++--- src/Avalonia.Controls/Remote/RemoteWidget.cs | 2 +- src/Avalonia.Controls/Shapes/Shape.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 8 +++++++- src/Avalonia.Controls/TickBar.cs | 2 +- 12 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 1bb574acd2..78ba23c1dd 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -225,7 +225,7 @@ namespace Avalonia.Controls /// Renders the control. /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { _borderRenderHelper.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray); diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index e4487d29fa..e1f840672d 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls _subscription?.Dispose(); } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) { diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 2501440ff2..ff9fb4e31d 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -3256,7 +3256,7 @@ namespace Avalonia.Controls /// /// UpdateRenderBounds. /// - public override void Render(DrawingContext drawingContext) + public sealed override void Render(DrawingContext drawingContext) { var grid = this.GetVisualParent(); diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 2cf0fc3ec4..3e76835e92 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls /// Renders the control. /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var source = Source; diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 007d18c813..a7dc035459 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -68,7 +68,7 @@ namespace Avalonia.Controls /// Renders the visual to a . /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var background = Background; if (background != null) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 584dfea97f..be61bb18a1 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -534,7 +534,7 @@ namespace Avalonia.Controls.Presenters } /// - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { _borderRenderer.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow); diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index f599511392..3481b1ecf3 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -388,7 +388,7 @@ namespace Avalonia.Controls.Presenters TextLayout.Draw(context, new Point(left, top)); } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 49e76d0728..ed3412bb45 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -60,10 +60,9 @@ namespace Avalonia.Controls.Primitives /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(DrawingContext context) + protected internal override void RenderCore(DrawingContext context) { - base.Render(context); - + base.RenderCore(context); int underscore = Text?.IndexOf('_') ?? -1; if (underscore != -1 && ShowAccessKey) diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 5cefb0d89f..b9854ab837 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -69,7 +69,7 @@ namespace Avalonia.Controls.Remote base.ArrangeCore(finalRect); } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { if (_lastFrame != null && _lastFrame.Width != 0 && _lastFrame.Height != 0) { diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index c8bf95b3f7..461dc1c947 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -193,7 +193,7 @@ namespace Avalonia.Controls.Shapes set { SetValue(StrokeJoinProperty, value); } } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var geometry = RenderedGeometry; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index ec31470126..df98d1073e 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -549,7 +549,13 @@ namespace Avalonia.Controls /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) + { + RenderCore(context); + } + + // Workaround to seal Render method, we need to make so because AccessText was overriding Render method which is sealed now. + internal protected virtual void RenderCore(DrawingContext context) { var background = Background; diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 4d902d3d5f..63fe9c9384 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -213,7 +213,7 @@ namespace Avalonia.Controls /// /// Brush that use to fill ticks is specified by Fill property. /// - public override void Render(DrawingContext dc) + public sealed override void Render(DrawingContext dc) { var size = new Size(Bounds.Width, Bounds.Height); var range = Maximum - Minimum; From 416ef6b601462f2eca229cfec1dd3481a8098aaa Mon Sep 17 00:00:00 2001 From: DJGosnell Date: Fri, 10 Feb 2023 18:45:20 -0500 Subject: [PATCH 036/108] Added two GetAndSetRadii methods. Added documentaiton. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 16 +---- src/Skia/Avalonia.Skia/SKRoundRectCache.cs | 76 +++++++++++++++++++- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2bb6f1dc7e..eededb2836 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -365,15 +365,7 @@ namespace Avalonia.Skia SKRoundRect skRoundRect = null; if (needRoundRect) { - skRoundRect = SKRoundRectCache.Shared.Get(); - skRoundRect.SetRectRadii(rc, - new[] - { - rect.RadiiTopLeft.ToSKPoint(), - rect.RadiiTopRight.ToSKPoint(), - rect.RadiiBottomRight.ToSKPoint(), - rect.RadiiBottomLeft.ToSKPoint(), - }); + skRoundRect = SKRoundRectCache.Shared.GetAndSetRadii(rc, rect); } foreach (var boxShadow in boxShadows) @@ -389,8 +381,7 @@ namespace Avalonia.Skia Canvas.Save(); if (isRounded) { - var shadowRect = SKRoundRectCache.Shared.Get(); - shadowRect.SetRectRadii(skRoundRect!.Rect, skRoundRect.Radii); + var shadowRect = SKRoundRectCache.Shared.GetAndSetRadii(skRoundRect!.Rect, skRoundRect.Radii); if (spread != 0) shadowRect.Inflate(spread, spread); Canvas.ClipRoundRect(skRoundRect, @@ -446,8 +437,7 @@ namespace Avalonia.Skia var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); Canvas.Save(); - var shadowRect = SKRoundRectCache.Shared.Get(); - shadowRect.SetRectRadii(skRoundRect!.Rect, skRoundRect.Radii); + var shadowRect = SKRoundRectCache.Shared.GetAndSetRadii(skRoundRect!.Rect, skRoundRect.Radii); if (spread != 0) shadowRect.Deflate(spread, spread); Canvas.ClipRoundRect(skRoundRect, diff --git a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs index e164f97d6a..8de9e65553 100644 --- a/src/Skia/Avalonia.Skia/SKRoundRectCache.cs +++ b/src/Skia/Avalonia.Skia/SKRoundRectCache.cs @@ -1,13 +1,73 @@ -using System.Collections.Concurrent; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; using SkiaSharp; namespace Avalonia.Skia { /// - /// Cache for SKPaints. + /// Cache for SKRoundRectCache. /// internal class SKRoundRectCache : SKCacheBase { + /// + /// Cache for points to use for setting the radii. + /// + private readonly ConcurrentBag _radiiCache = new(); + + /// + /// Gets a cached SKRoundRect and sets it with the passed rectangle and Radii. + /// + /// Rectangle size to set the cached rectangle to. + /// Rounded rectangle to copy the radii from. + /// Configured rounded rectangle + public SKRoundRect GetAndSetRadii(in SKRect rectangle, in RoundedRect roundedRect) + { + if (!Cache.TryTake(out var item)) + { + item = new SKRoundRect(); + } + + // Try and acquire a cached point array. + if (!_radiiCache.TryTake(out var skArray)) + { + skArray = new SKPoint[4]; + } + + skArray[0].X = (float)roundedRect.RadiiTopLeft.X; + skArray[0].Y = (float)roundedRect.RadiiTopLeft.Y; + skArray[1].X = (float)roundedRect.RadiiTopRight.X; + skArray[1].Y = (float)roundedRect.RadiiTopRight.Y; + skArray[2].X = (float)roundedRect.RadiiBottomRight.X; + skArray[2].Y = (float)roundedRect.RadiiBottomRight.Y; + skArray[3].X = (float)roundedRect.RadiiBottomLeft.X; + skArray[3].Y = (float)roundedRect.RadiiBottomLeft.Y; + + item.SetRectRadii(rectangle, skArray); + + // Add the array back to the cache. + _radiiCache.Add(skArray); + + return item; + } + + /// + /// Gets a cached SKRoundRect and sets it with the passed rectangle and Radii. + /// + /// Rectangle size to set the cached rectangle to. + /// point array of radii. + /// Configured rounded rectangle + public SKRoundRect GetAndSetRadii(in SKRect rectangle, in SKPoint[] radii) + { + if (!Cache.TryTake(out var item)) + { + item = new SKRoundRect(); + } + + item.SetRectRadii(rectangle, radii); + + return item; + } /// /// Returns a SKPaint and resets it for reuse later. /// @@ -22,5 +82,17 @@ namespace Avalonia.Skia rect.SetEmpty(); Cache.Add(rect); } + + /// + /// Clears and disposes all cached items. + /// + public new void Clear() + { + base.Clear(); + + // Clear out the cache of SKPoint arrays. + _radiiCache.Clear(); + } + } } From 0d32343410c615809d87f1a06b1c6349f4b95b1a Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 8 Feb 2023 10:13:26 +0100 Subject: [PATCH 037/108] Nullability annotations for Avalonia.Win32 --- .../Platform/IPlatformThreadingInterface.cs | 2 +- .../Platform/Storage/FilePickerFileType.cs | 4 +- src/Avalonia.Base/Rendering/IRenderTimer.cs | 1 - .../Platform/IPlatformLifetimeEventsImpl.cs | 2 +- src/Avalonia.Controls/Platform/IWindowImpl.cs | 8 +- src/Avalonia.Controls/WindowBase.cs | 2 +- src/Windows/Avalonia.Win32/AngleOptions.cs | 2 +- .../AutomationNode.ExpandCollapse.cs | 2 - .../Automation/AutomationNode.RangeValue.cs | 2 - .../Automation/AutomationNode.Scroll.cs | 2 - .../Automation/AutomationNode.Selection.cs | 8 +- .../Automation/AutomationNode.Toggle.cs | 2 - .../Automation/AutomationNode.Value.cs | 2 - .../Automation/AutomationNode.cs | 10 +- .../Automation/RootAutomationNode.cs | 2 - .../Avalonia.Win32/Avalonia.Win32.csproj | 4 +- .../Avalonia.Win32/ClipboardFormats.cs | 30 +-- src/Windows/Avalonia.Win32/ClipboardImpl.cs | 18 +- src/Windows/Avalonia.Win32/DataObject.cs | 37 ++-- .../Avalonia.Win32/DirectX/DirectXStructs.cs | 8 - .../Avalonia.Win32/DirectX/DxgiConnection.cs | 28 +-- .../DirectX/DxgiRenderTarget.cs | 41 ++-- .../Avalonia.Win32/FramebufferManager.cs | 2 - .../Avalonia.Win32/Input/Imm32InputMethod.cs | 78 +++---- .../Input/WindowsMouseDevice.cs | 9 +- .../Interop/Automation/IGridProvider.cs | 3 +- .../Automation/IRawElementProviderFragment.cs | 2 - .../IRawElementProviderFragmentRoot.cs | 5 +- .../Automation/IRawElementProviderSimple.cs | 2 - .../Automation/IRawElementProviderSimple2.cs | 5 +- .../Automation/ISelectionItemProvider.cs | 2 - .../Interop/Automation/IValueProvider.cs | 3 - .../Interop/Automation/UiaCoreProviderApi.cs | 10 +- .../Interop/Automation/UiaCoreTypesApi.cs | 4 +- .../Avalonia.Win32/Interop/TaskBarList.cs | 15 +- .../Interop/UnmanagedMethods.cs | 54 ++--- .../Avalonia.Win32/NonPumpingSyncContext.cs | 19 +- .../Avalonia.Win32/OffscreenParentWindow.cs | 9 +- src/Windows/Avalonia.Win32/OleContext.cs | 12 +- src/Windows/Avalonia.Win32/OleDataObject.cs | 17 +- src/Windows/Avalonia.Win32/OleDropTarget.cs | 43 ++-- .../OpenGl/Angle/AngleD3DTextureFeature.cs | 6 +- .../OpenGl/Angle/AngleEglInterface.cs | 2 +- .../Angle/AngleExternalD3D11Texture2D.cs | 30 ++- .../Angle/AngleExternalObjectsFeature.cs | 6 +- .../OpenGl/Angle/AngleWin32EglDisplay.cs | 3 +- .../Angle/AngleWin32PlatformGraphics.cs | 16 +- .../Avalonia.Win32/OpenGl/WglContext.cs | 16 +- .../Avalonia.Win32/OpenGl/WglDisplay.cs | 45 ++-- .../OpenGl/WglGdiResourceManager.cs | 122 ++++++----- .../OpenGl/WglPlatformOpenGlInterface.cs | 18 +- .../OpenGl/WglRestoreContext.cs | 4 +- src/Windows/Avalonia.Win32/PopupImpl.cs | 15 +- src/Windows/Avalonia.Win32/ScreenImpl.cs | 19 +- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 32 ++- src/Windows/Avalonia.Win32/Win32GlManager.cs | 17 +- .../Avalonia.Win32/Win32NativeControlHost.cs | 42 ++-- .../Win32NativeToManagedMenuExporter.cs | 9 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 50 ++--- .../Avalonia.Win32/Win32PlatformSettings.cs | 8 +- .../Avalonia.Win32/Win32StorageProvider.cs | 12 +- .../WinRT/Composition/WinUIEffectBase.cs | 27 ++- .../Composition/WinUiCompositedWindow.cs | 7 +- .../WinUiCompositedWindowSurface.cs | 49 +++-- .../Composition/WinUiCompositionShared.cs | 10 +- .../Composition/WinUiCompositionUtils.cs | 11 +- .../Composition/WinUiCompositorConnection.cs | 16 +- .../WinRT/NativeWinRTMethods.cs | 20 +- .../Avalonia.Win32/WinRT/WinRTInspectable.cs | 7 +- src/Windows/Avalonia.Win32/WinScreen.cs | 8 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 82 ++++--- .../WindowImpl.CustomCaptionProc.cs | 8 +- .../Avalonia.Win32/WindowImpl.WndProc.cs | 7 - src/Windows/Avalonia.Win32/WindowImpl.cs | 206 +++++++++++------- .../WindowsMountedVolumeInfoListener.cs | 13 +- src/tools/DevGenerators/Helpers.cs | 11 +- 76 files changed, 713 insertions(+), 752 deletions(-) diff --git a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs index bf18a7da5b..3dbc7c1bb2 100644 --- a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs @@ -26,6 +26,6 @@ namespace Avalonia.Platform bool CurrentThreadIsLoopThread { get; } - event Action Signaled; + event Action? Signaled; } } diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs index 75076e2bb8..f9c7f9685d 100644 --- a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs @@ -7,9 +7,9 @@ namespace Avalonia.Platform.Storage; /// public sealed class FilePickerFileType { - public FilePickerFileType(string name) + public FilePickerFileType(string? name) { - Name = name; + Name = name ?? string.Empty; } /// diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs index 07af7eeec8..14fcffd6a9 100644 --- a/src/Avalonia.Base/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using Avalonia.Metadata; namespace Avalonia.Rendering diff --git a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs index 0658f9211c..d609dd94c8 100644 --- a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs +++ b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs @@ -13,6 +13,6 @@ namespace Avalonia.Platform /// /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. /// - event EventHandler ShutdownRequested; + event EventHandler? ShutdownRequested; } } diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 8d9d8e0e7b..31b144ce00 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -19,7 +19,7 @@ namespace Avalonia.Platform /// /// Gets or sets a method called when the minimized/maximized state of the window changes. /// - Action WindowStateChanged { get; set; } + Action? WindowStateChanged { get; set; } /// /// Sets the title of the window. @@ -42,7 +42,7 @@ namespace Avalonia.Platform /// /// Called when a disabled window received input. Can be used to activate child windows. /// - Action GotInputWhenDisabled { get; set; } + Action? GotInputWhenDisabled { get; set; } /// /// Enables or disables system window decorations (title bar, buttons, etc) @@ -68,7 +68,7 @@ namespace Avalonia.Platform /// Gets or sets a method called before the underlying implementation is destroyed. /// Return true to prevent the underlying implementation from closing. /// - Func Closing { get; set; } + Func? Closing { get; set; } /// /// Gets a value to indicate if the platform was able to extend client area to non-client area. @@ -78,7 +78,7 @@ namespace Avalonia.Platform /// /// Gets or Sets an action that is called whenever one of the extend client area properties changed. /// - Action ExtendClientAreaToDecorationsChanged { get; set; } + Action? ExtendClientAreaToDecorationsChanged { get; set; } /// /// Gets a flag that indicates if Managed decorations i.e. caption buttons are required. diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 0c9a91148b..26e11f0d4a 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -94,7 +94,7 @@ namespace Avalonia.Controls private set { SetAndRaise(IsActiveProperty, ref _isActive, value); } } - public Screens Screens { get; private set; } + public Screens Screens { get; } /// /// Gets or sets the owner of the window. diff --git a/src/Windows/Avalonia.Win32/AngleOptions.cs b/src/Windows/Avalonia.Win32/AngleOptions.cs index 076ddd2a5d..94c67120df 100644 --- a/src/Windows/Avalonia.Win32/AngleOptions.cs +++ b/src/Windows/Avalonia.Win32/AngleOptions.cs @@ -17,6 +17,6 @@ namespace Avalonia.Win32 new GlVersion(GlProfileType.OpenGLES, 2, 0) }; - public IList AllowedPlatformApis { get; set; } = null; + public IList? AllowedPlatformApis { get; set; } = null; } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs index 5f3f863493..aaad9cb3ba 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs @@ -2,8 +2,6 @@ using Avalonia.Automation.Provider; using UIA = Avalonia.Win32.Interop.Automation; -#nullable enable - namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IExpandCollapseProvider diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs index b91cb76888..518c945aa9 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs @@ -1,8 +1,6 @@ using Avalonia.Automation.Provider; using UIA = Avalonia.Win32.Interop.Automation; -#nullable enable - namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IRangeValueProvider diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs index 4f2d4ae269..0248a8d6a7 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs @@ -1,8 +1,6 @@ using Avalonia.Automation.Provider; using UIA = Avalonia.Win32.Interop.Automation; -#nullable enable - namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IScrollProvider, UIA.IScrollItemProvider diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs index 61903ab5b0..3751bb6476 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Avalonia.Automation.Peers; using Avalonia.Automation.Provider; using UIA = Avalonia.Win32.Interop.Automation; -#nullable enable - namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.ISelectionProvider, UIA.ISelectionItemProvider @@ -27,8 +24,7 @@ namespace Avalonia.Win32.Automation UIA.IRawElementProviderSimple[] UIA.ISelectionProvider.GetSelection() { var peers = InvokeSync>(x => x.GetSelection()); - return peers?.Select(x => (UIA.IRawElementProviderSimple)GetOrCreate(x)!).ToArray() ?? - Array.Empty(); + return peers.Select(x => (UIA.IRawElementProviderSimple)GetOrCreate(x)).ToArray(); } void UIA.ISelectionItemProvider.AddToSelection() => InvokeSync(x => x.AddToSelection()); diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs index 38f4d80946..08f4b62d83 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs @@ -1,8 +1,6 @@ using Avalonia.Automation.Provider; using UIA = Avalonia.Win32.Interop.Automation; -#nullable enable - namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IToggleProvider diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs index 34f5dfe0b9..204e159049 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs @@ -2,8 +2,6 @@ using Avalonia.Automation.Provider; using UIA = Avalonia.Win32.Interop.Automation; -#nullable enable - namespace Avalonia.Win32.Automation { internal partial class AutomationNode : UIA.IValueProvider diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 29ab2cea3a..5ca4ef63bf 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -12,8 +12,6 @@ using Avalonia.Threading; using Avalonia.Win32.Interop.Automation; using AAP = Avalonia.Automation.Provider; -#nullable enable - namespace Avalonia.Win32.Automation { [ComVisible(true)] @@ -25,7 +23,7 @@ namespace Avalonia.Win32.Automation IRawElementProviderAdviseEvents, IInvokeProvider { - private static Dictionary s_propertyMap = new Dictionary() + private static Dictionary s_propertyMap = new() { { AutomationElementIdentifiers.BoundingRectangleProperty, UiaPropertyId.BoundingRectangle }, { AutomationElementIdentifiers.ClassNameProperty, UiaPropertyId.ClassName }, @@ -46,8 +44,7 @@ namespace Avalonia.Win32.Automation { SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection }, }; - private static ConditionalWeakTable s_nodes = - new ConditionalWeakTable(); + private static ConditionalWeakTable s_nodes = new(); private readonly int[] _runtimeId; private int _raiseFocusChanged; @@ -174,11 +171,12 @@ namespace Avalonia.Win32.Automation NavigateDirection.LastChild => GetOrCreate(Peer.GetChildren().LastOrDefault()), _ => null, }; - }) as IRawElementProviderFragment; + }); } public void SetFocus() => InvokeSync(() => Peer.SetFocus()); + [return: NotNullIfNotNull(nameof(peer))] public static AutomationNode? GetOrCreate(AutomationPeer? peer) { return peer is null ? null : s_nodes.GetValue(peer, Create); diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index b732c4169f..3d8b4995ad 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -6,8 +6,6 @@ using Avalonia.Automation.Provider; using Avalonia.Platform; using Avalonia.Win32.Interop.Automation; -#nullable enable - namespace Avalonia.Win32.Automation { [RequiresUnreferencedCode("Requires .NET COM interop")] diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index a24fe31df8..754e883c2f 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -6,7 +6,6 @@ - @@ -16,7 +15,8 @@ - + + diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index 7bd7765f8c..5fc4f21b2e 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -8,15 +8,15 @@ using Avalonia.Utilities; namespace Avalonia.Win32 { - static class ClipboardFormats + internal static class ClipboardFormats { private const int MAX_FORMAT_NAME_LENGTH = 260; - class ClipboardFormat + private class ClipboardFormat { - public ushort Format { get; private set; } - public string Name { get; private set; } - public ushort[] Synthesized { get; private set; } + public ushort Format { get; } + public string Name { get; } + public ushort[] Synthesized { get; } public ClipboardFormat(string name, ushort format, params ushort[] synthesized) { @@ -26,14 +26,14 @@ namespace Avalonia.Win32 } } - private static readonly List FormatList = new List() + private static readonly List s_formatList = new() { new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT), new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), }; - private static string QueryFormatName(ushort format) + private static string? QueryFormatName(ushort format) { var sb = StringBuilderCache.Acquire(MAX_FORMAT_NAME_LENGTH); if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0) @@ -43,16 +43,16 @@ namespace Avalonia.Win32 public static string GetFormat(ushort format) { - lock (FormatList) + lock (s_formatList) { - var pd = FormatList.FirstOrDefault(f => f.Format == format || Array.IndexOf(f.Synthesized, format) >= 0); + var pd = s_formatList.FirstOrDefault(f => f.Format == format || Array.IndexOf(f.Synthesized, format) >= 0); if (pd == null) { - string name = QueryFormatName(format); + string? name = QueryFormatName(format); if (string.IsNullOrEmpty(name)) - name = string.Format("Unknown_Format_{0}", format); + name = $"Unknown_Format_{format}"; pd = new ClipboardFormat(name, format); - FormatList.Add(pd); + s_formatList.Add(pd); } return pd.Name; } @@ -60,16 +60,16 @@ namespace Avalonia.Win32 public static ushort GetFormat(string format) { - lock (FormatList) + lock (s_formatList) { - var pd = FormatList.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Name, format)); + var pd = s_formatList.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Name, format)); if (pd == null) { int id = UnmanagedMethods.RegisterClipboardFormat(format); if (id == 0) throw new Win32Exception(); pd = new ClipboardFormat(format, (ushort)id); - FormatList.Add(pd); + s_formatList.Add(pd); } return pd.Format; } diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 82fd1109f4..1a760aeab8 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -30,7 +30,7 @@ namespace Avalonia.Win32 return Disposable.Create(() => UnmanagedMethods.CloseClipboard()); } - public async Task GetTextAsync() + public async Task GetTextAsync() { using(await OpenClipboard()) { @@ -52,19 +52,17 @@ namespace Avalonia.Win32 } } - public async Task SetTextAsync(string text) + public async Task SetTextAsync(string? text) { - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } - using(await OpenClipboard()) { UnmanagedMethods.EmptyClipboard(); - var hGlobal = Marshal.StringToHGlobalUni(text); - UnmanagedMethods.SetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, hGlobal); + if (text is not null) + { + var hGlobal = Marshal.StringToHGlobalUni(text); + UnmanagedMethods.SetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, hGlobal); + } } } @@ -121,7 +119,7 @@ namespace Avalonia.Win32 } } - public async Task GetDataAsync(string format) + public async Task GetDataAsync(string format) { Dispatcher.UIThread.VerifyAccess(); var i = OleRetryCount; diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index 7d22e57a30..272300cbf3 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -22,9 +22,9 @@ namespace Avalonia.Win32 // Compatibility with WinForms + WPF... internal static readonly byte[] SerializedObjectGUID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray(); - class FormatEnumerator : CallbackBase, Win32Com.IEnumFORMATETC + private class FormatEnumerator : CallbackBase, Win32Com.IEnumFORMATETC { - private FORMATETC[] _formats; + private readonly FORMATETC[] _formats; private uint _current; private FormatEnumerator(FORMATETC[] formats, uint current) @@ -105,16 +105,12 @@ namespace Avalonia.Win32 public DataObject(IDataObject wrapped) { - if (wrapped == null) + _wrapped = wrapped switch { - throw new ArgumentNullException(nameof(wrapped)); - } - if (_wrapped is DataObject || _wrapped is OleDataObject) - { - throw new InvalidOperationException(); - } - - _wrapped = wrapped; + null => throw new ArgumentNullException(nameof(wrapped)), + DataObject or OleDataObject => throw new ArgumentException($"Cannot wrap a {wrapped.GetType()}"), + _ => wrapped + }; } #region IDataObject @@ -128,17 +124,17 @@ namespace Avalonia.Win32 return _wrapped.GetDataFormats(); } - IEnumerable IDataObject.GetFileNames() + IEnumerable? IDataObject.GetFileNames() { return _wrapped.GetFileNames(); } - string IDataObject.GetText() + string? IDataObject.GetText() { return _wrapped.GetText(); } - object IDataObject.Get(string dataFormat) + object? IDataObject.Get(string dataFormat) { return _wrapped.Get(dataFormat); } @@ -182,9 +178,6 @@ namespace Avalonia.Win32 if (_wrapped is Win32Com.IDataObject ole) return ole.GetCanonicalFormatEtc(formatIn); - var formatOut = new FORMATETC(); - formatOut.ptd = IntPtr.Zero; - throw new COMException(nameof(UnmanagedMethods.HRESULT.E_NOTIMPL), unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); } @@ -264,9 +257,9 @@ namespace Avalonia.Win32 private uint WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal) { - object data = _wrapped.Get(dataFormat); + object data = _wrapped.Get(dataFormat)!; if (dataFormat == DataFormats.Text || data is string) - return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data)); + return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data) ?? string.Empty); if (dataFormat == DataFormats.FileNames && data is IEnumerable files) return WriteFileListToHGlobal(ref hGlobal, files); if (data is Stream stream) @@ -286,7 +279,7 @@ namespace Avalonia.Win32 } if (data is IEnumerable bytes) { - var byteArr = bytes is byte[] ? (byte[])bytes : bytes.ToArray(); + var byteArr = bytes as byte[] ?? bytes.ToArray(); return WriteBytesToHGlobal(ref hGlobal, byteArr); } return WriteBytesToHGlobal(ref hGlobal, SerializeObject(data)); @@ -332,7 +325,7 @@ namespace Avalonia.Win32 private static uint WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files) { - if (!files?.Any() ?? false) + if (!files.Any()) return unchecked((int)UnmanagedMethods.HRESULT.S_OK); char[] filesStr = (string.Join("\0", files) + "\0\0").ToCharArray(); @@ -392,7 +385,7 @@ namespace Avalonia.Win32 public void ReleaseWrapped() { - _wrapped = null; + _wrapped = null!; } #endregion } diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs index c11a6026e7..e59eb98c6c 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs @@ -1,17 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; using static Avalonia.Win32.Interop.UnmanagedMethods; // ReSharper disable InconsistentNaming #pragma warning disable CS0649 namespace Avalonia.Win32.DirectX { -#nullable enable public unsafe struct HANDLE { public readonly void* Value; @@ -288,5 +281,4 @@ namespace Avalonia.Win32.DirectX public D3D11_RESOURCE_MISC_FLAG MiscFlags; } -#nullable restore } diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs index e82f7633be..26696ef903 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs @@ -1,24 +1,14 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; using Avalonia.Logging; -using Avalonia.OpenGL.Angle; -using Avalonia.OpenGL.Egl; using Avalonia.Rendering; -using Avalonia.Win32.OpenGl.Angle; using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.DirectX.DirectXUnmanagedMethods; using MicroCom.Runtime; namespace Avalonia.Win32.DirectX { -#pragma warning disable CA1416 // This should only be reachable on Windows -#nullable enable public unsafe class DxgiConnection : IRenderTimer { public const uint ENUM_CURRENT_SETTINGS = unchecked((uint)(-1)); @@ -26,11 +16,11 @@ namespace Avalonia.Win32.DirectX public bool RunsInBackground => true; public event Action? Tick; - private object _syncLock; + private readonly object _syncLock; - private IDXGIOutput? _output = null; + private IDXGIOutput? _output; - private Stopwatch? _stopwatch = null; + private Stopwatch? _stopwatch; private const string LogArea = "DXGI"; public DxgiConnection(object syncLock) @@ -51,9 +41,9 @@ namespace Avalonia.Win32.DirectX } } - private unsafe void RunLoop() + private void RunLoop() { - _stopwatch = System.Diagnostics.Stopwatch.StartNew(); + _stopwatch = Stopwatch.StartNew(); try { GetBestOutputToVWaitOn(); @@ -162,7 +152,7 @@ namespace Avalonia.Win32.DirectX } // Used the windows composition as a blueprint for this startup/creation - static private bool TryCreateAndRegisterCore() + private static bool TryCreateAndRegisterCore() { var tcs = new TaskCompletionSource(); var pumpLock = new object(); @@ -170,9 +160,7 @@ namespace Avalonia.Win32.DirectX { try { - DxgiConnection connection; - - connection = new DxgiConnection(pumpLock); + var connection = new DxgiConnection(pumpLock); AvaloniaLocator.CurrentMutable.BindToSelf(connection); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); @@ -191,6 +179,4 @@ namespace Avalonia.Win32.DirectX return tcs.Task.Result; } } -#nullable restore -#pragma warning restore CA1416 // Validate platform compatibility } diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs index cb7826e185..065386b401 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs @@ -1,40 +1,28 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Win32.OpenGl.Angle; using MicroCom.Runtime; -using static Avalonia.OpenGL.Egl.EglGlPlatformSurfaceBase; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32.DirectX { -#pragma warning disable CA1416 // Validate platform compatibility, if you enter this not on windows you have messed up badly -#nullable enable public unsafe class DxgiRenderTarget : EglPlatformSurfaceRenderTargetBase { // DXGI_FORMAT_B8G8R8A8_UNORM is target texture format as per ANGLE documentation public const uint DXGI_USAGE_RENDER_TARGET_OUTPUT = 0x00000020U; + private readonly Guid ID3D11Texture2DGuid = Guid.Parse("6F15AAF2-D208-4E89-9AB4-489535D34F9C"); - private EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _window; - private DxgiConnection _connection; - private IDXGIDevice? _dxgiDevice = null; - private IDXGIFactory2? _dxgiFactory = null; - private IDXGISwapChain1? _swapChain = null; - private IUnknown? _renderTexture = null; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _window; + private readonly DxgiConnection _connection; + private readonly IDXGIDevice? _dxgiDevice; + private readonly IDXGIFactory2? _dxgiFactory; + private readonly IDXGISwapChain1? _swapChain; + private readonly uint _flagsUsed; - private Interop.UnmanagedMethods.RECT _clientRect = default; - - private uint _flagsUsed; - - private Guid ID3D11Texture2DGuid = Guid.Parse("6F15AAF2-D208-4E89-9AB4-489535D34F9C"); + private IUnknown? _renderTexture; + private RECT _clientRect; public DxgiRenderTarget(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo window, EglContext context, DxgiConnection connection) : base(context) { @@ -80,11 +68,11 @@ namespace Avalonia.Win32.DirectX null ); - Interop.UnmanagedMethods.RECT pClientRect; - GetClientRect(_window.Handle, out pClientRect); + GetClientRect(_window.Handle, out var pClientRect); _clientRect = pClientRect; } + /// public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { if (_swapChain is null) @@ -98,8 +86,7 @@ namespace Avalonia.Win32.DirectX var success = false; try { - Interop.UnmanagedMethods.RECT pClientRect; - GetClientRect(_window.Handle, out pClientRect); + GetClientRect(_window.Handle, out var pClientRect); if (!RectsEqual(pClientRect, _clientRect)) { // we gotta resize @@ -137,7 +124,7 @@ namespace Avalonia.Win32.DirectX var res = base.BeginDraw(surface, _window.Size, _window.Scaling, () => { _swapChain.Present((ushort)0U, (ushort)0U); - surface?.Dispose(); + surface.Dispose(); transaction?.Dispose(); contextLock?.Dispose(); }, true); @@ -178,6 +165,4 @@ namespace Avalonia.Win32.DirectX } } -#pragma warning restore CA1416 // Validate platform compatibility -#nullable restore } diff --git a/src/Windows/Avalonia.Win32/FramebufferManager.cs b/src/Windows/Avalonia.Win32/FramebufferManager.cs index 8feecab4dd..cba4879f5b 100644 --- a/src/Windows/Avalonia.Win32/FramebufferManager.cs +++ b/src/Windows/Avalonia.Win32/FramebufferManager.cs @@ -4,8 +4,6 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.Platform; using Avalonia.Win32.Interop; -#nullable enable - namespace Avalonia.Win32 { internal class FramebufferManager : IFramebufferPlatformSurface, IDisposable diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 7ef1cb1d1c..41417dd950 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text; using Avalonia.Input.TextInput; using Avalonia.Threading; @@ -10,21 +11,21 @@ namespace Avalonia.Win32.Input /// /// A Windows input method editor based on Windows Input Method Manager (IMM32). /// - class Imm32InputMethod : ITextInputMethodImpl + internal class Imm32InputMethod : ITextInputMethodImpl { - public IntPtr HWND { get; private set; } + public IntPtr Hwnd { get; private set; } private IntPtr _currentHimc; - private WindowImpl _parent; - private ITextInputMethodClient _client; + private WindowImpl? _parent; - private Imm32CaretManager _caretManager = new(); + private Imm32CaretManager _caretManager; private ushort _langId; - private const int _caretMargin = 1; + private const int CaretMargin = 1; - public ITextInputMethodClient Client => _client; + public ITextInputMethodClient? Client { get; private set; } - public bool IsActive => _client != null; + [MemberNotNullWhen(true, nameof(Client))] + public bool IsActive => Client != null; public bool IsComposing { get; set; } @@ -32,12 +33,12 @@ namespace Avalonia.Win32.Input public void CreateCaret() { - _caretManager.TryCreate(HWND); + _caretManager.TryCreate(Hwnd); } public void EnableImm() { - var himc = ImmGetContext(HWND); + var himc = ImmGetContext(Hwnd); if(himc == IntPtr.Zero) { @@ -51,13 +52,13 @@ namespace Avalonia.Win32.Input DisableImm(); } - ImmAssociateContext(HWND, himc); + ImmAssociateContext(Hwnd, himc); - ImmReleaseContext(HWND, himc); + ImmReleaseContext(Hwnd, himc); _currentHimc = himc; - _caretManager.TryCreate(HWND); + _caretManager.TryCreate(Hwnd); } } @@ -67,7 +68,7 @@ namespace Avalonia.Win32.Input Reset(); - ImmAssociateContext(HWND, IntPtr.Zero); + ImmAssociateContext(Hwnd, IntPtr.Zero); _caretManager.TryDestroy(); @@ -76,7 +77,7 @@ namespace Avalonia.Win32.Input public void SetLanguageAndWindow(WindowImpl parent, IntPtr hwnd, IntPtr HKL) { - HWND = hwnd; + Hwnd = hwnd; _parent = parent; _langId = PRIMARYLANGID(LGID(HKL)); @@ -98,9 +99,9 @@ namespace Avalonia.Win32.Input { DisableImm(); - HWND = IntPtr.Zero; + Hwnd = IntPtr.Zero; _parent = null; - _client = null; + Client = null; _langId = 0; IsComposing = false; @@ -108,13 +109,13 @@ namespace Avalonia.Win32.Input //Dependant on CurrentThread. When Avalonia will support Multiple Dispatchers - //every Dispatcher should have their own InputMethod. - public static Imm32InputMethod Current { get; } = new Imm32InputMethod(); + public static Imm32InputMethod Current { get; } = new(); public void Reset() { Dispatcher.UIThread.Post(() => { - var himc = ImmGetContext(HWND); + var himc = ImmGetContext(Hwnd); if (IsComposing) { @@ -123,13 +124,13 @@ namespace Avalonia.Win32.Input IsComposing = false; } - ImmReleaseContext(HWND, himc); + ImmReleaseContext(Hwnd, himc); }); } - public void SetClient(ITextInputMethodClient client) + public void SetClient(ITextInputMethodClient? client) { - _client = client; + Client = client; Dispatcher.UIThread.Post(() => { @@ -152,7 +153,7 @@ namespace Avalonia.Win32.Input public void SetCursorRect(Rect rect) { - var focused = GetActiveWindow() == HWND; + var focused = GetActiveWindow() == Hwnd; if (!focused) { @@ -161,7 +162,7 @@ namespace Avalonia.Win32.Input Dispatcher.UIThread.Post(() => { - var himc = ImmGetContext(HWND); + var himc = ImmGetContext(Hwnd); if (himc == IntPtr.Zero) { @@ -170,7 +171,7 @@ namespace Avalonia.Win32.Input MoveImeWindow(rect, himc); - ImmReleaseContext(HWND, himc); + ImmReleaseContext(Hwnd, himc); }); } @@ -219,7 +220,7 @@ namespace Avalonia.Win32.Input // the caret to move the position of their candidate windows. // On the other hand, Korean IMEs require the lower-left corner of the // caret to move their candidate windows. - y2 += _caretMargin; + y2 += CaretMargin; } // Need to return here since some Chinese IMEs would stuck if set @@ -238,7 +239,7 @@ namespace Avalonia.Win32.Input dwIndex = 0, dwStyle = CFS_EXCLUDE, ptCurrentPos = new POINT {X = x1, Y = y1}, - rcArea = new RECT {left = x1, top = y1, right = x2, bottom = y2 + _caretMargin} + rcArea = new RECT {left = x1, top = y1, right = x2, bottom = y2 + CaretMargin} }; ImmSetCandidateWindow(himc, ref excludeRectangle); @@ -275,34 +276,21 @@ namespace Avalonia.Win32.Input return; } - if(!IsActive || !_client.SupportsPreedit) + if(!IsActive || !Client.SupportsPreedit) { return; } var composition = GetCompositionString(); - _client.SetPreeditText(composition); + Client.SetPreeditText(composition); } - private string GetCompositionString() + private string? GetCompositionString() { - var himc = ImmGetContext(HWND); + var himc = ImmGetContext(Hwnd); - var length = ImmGetCompositionString(himc, GCS.GCS_COMPSTR, IntPtr.Zero, 0); - - var buffer = new byte[length]; - - unsafe - { - fixed (byte* bufferPtr = buffer) - { - var error = ImmGetCompositionString(himc, GCS.GCS_COMPSTR, (IntPtr)bufferPtr, (uint)length); - - return Encoding.Unicode.GetString(buffer, 0, buffer.Length); - } - } - + return ImmGetCompositionString(himc, GCS.GCS_COMPSTR); } ~Imm32InputMethod() diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index 998ff4a427..c7bc511b99 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -5,9 +5,10 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - class WindowsMouseDevice : MouseDevice + internal class WindowsMouseDevice : MouseDevice { private readonly IPointer _pointer; + public WindowsMouseDevice() : base(WindowsMousePointer.CreatePointer(out var pointer)) { _pointer = pointer; @@ -15,14 +16,14 @@ namespace Avalonia.Win32.Input // Normally user should use IPointer.Capture instead of MouseDevice.Capture, // But on Windows we need to handle WM_MOUSE capture manually without having access to the Pointer. - internal void Capture(IInputElement control) + internal void Capture(IInputElement? control) { _pointer.Capture(control); } internal class WindowsMousePointer : Pointer { - private WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true) + private WindowsMousePointer() : base(GetNextFreeId(),PointerType.Mouse, true) { } @@ -31,7 +32,7 @@ namespace Avalonia.Win32.Input return pointer = new WindowsMousePointer(); } - protected override void PlatformCapture(IInputElement element) + protected override void PlatformCapture(IInputElement? element) { var hwnd = (TopLevel.GetTopLevel(element as Visual)?.PlatformImpl as WindowImpl) ?.Handle.Handle; diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs index a8caf26524..be8b998a9e 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; namespace Avalonia.Win32.Interop.Automation @@ -8,7 +7,7 @@ namespace Avalonia.Win32.Interop.Automation [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IGridProvider { - IRawElementProviderSimple GetItem(int row, int column); + IRawElementProviderSimple? GetItem(int row, int column); int RowCount { get; } int ColumnCount { get; } } diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs index a62aa842cb..46d8dc6cdb 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs @@ -1,8 +1,6 @@ using System; using System.Runtime.InteropServices; -#nullable enable - namespace Avalonia.Win32.Interop.Automation { [ComVisible(true)] diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs index 71d1bdce60..1fd0cb13ba 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; namespace Avalonia.Win32.Interop.Automation @@ -8,7 +7,7 @@ namespace Avalonia.Win32.Interop.Automation [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IRawElementProviderFragmentRoot : IRawElementProviderFragment { - IRawElementProviderFragment ElementProviderFromPoint(double x, double y); - IRawElementProviderFragment GetFocus(); + IRawElementProviderFragment? ElementProviderFromPoint(double x, double y); + IRawElementProviderFragment? GetFocus(); } } diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs index 439036290e..5b67d10206 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs @@ -1,8 +1,6 @@ using System; using System.Runtime.InteropServices; -#nullable enable - namespace Avalonia.Win32.Interop.Automation { [Flags] diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs index f3504b8d77..c2e8a42f87 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs @@ -1,7 +1,4 @@ -using System; -using System.Runtime.InteropServices; - -#nullable enable +using System.Runtime.InteropServices; namespace Avalonia.Win32.Interop.Automation { diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs index 8d6677315c..072e210027 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs @@ -1,5 +1,3 @@ -#nullable enable -using System; using System.Runtime.InteropServices; namespace Avalonia.Win32.Interop.Automation diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs b/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs index 919be647f8..0356f93ab8 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/IValueProvider.cs @@ -1,8 +1,5 @@ -using System; using System.Runtime.InteropServices; -#nullable enable - namespace Avalonia.Win32.Interop.Automation { [ComVisible(true)] diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs index 4ba7a710d4..bd939c56b7 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreProviderApi.cs @@ -71,21 +71,21 @@ namespace Avalonia.Win32.Interop.Automation public static extern bool UiaClientsAreListening(); [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern IntPtr UiaReturnRawElementProvider(IntPtr hwnd, IntPtr wParam, IntPtr lParam, IRawElementProviderSimple el); + public static extern IntPtr UiaReturnRawElementProvider(IntPtr hwnd, IntPtr wParam, IntPtr lParam, IRawElementProviderSimple? el); [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] public static extern int UiaHostProviderFromHwnd(IntPtr hwnd, [MarshalAs(UnmanagedType.Interface)] out IRawElementProviderSimple provider); [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseAutomationEvent(IRawElementProviderSimple provider, int id); + public static extern int UiaRaiseAutomationEvent(IRawElementProviderSimple? provider, int id); [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseAutomationPropertyChangedEvent(IRawElementProviderSimple provider, int id, object oldValue, object newValue); + public static extern int UiaRaiseAutomationPropertyChangedEvent(IRawElementProviderSimple? provider, int id, object? oldValue, object? newValue); [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaRaiseStructureChangedEvent(IRawElementProviderSimple provider, StructureChangeType structureChangeType, int[] runtimeId, int runtimeIdLen); + public static extern int UiaRaiseStructureChangedEvent(IRawElementProviderSimple? provider, StructureChangeType structureChangeType, int[]? runtimeId, int runtimeIdLen); [DllImport("UIAutomationCore.dll", CharSet = CharSet.Unicode)] - public static extern int UiaDisconnectProvider(IRawElementProviderSimple provider); + public static extern int UiaDisconnectProvider(IRawElementProviderSimple? provider); } } diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs index 08b3ee32fa..b6b069ac37 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs @@ -63,8 +63,8 @@ namespace Avalonia.Win32.Interop.Automation } #endif - var comConfig = AppContext.GetData("System.Runtime.InteropServices.BuiltInComInterop.IsSupported"); - return comConfig == null || bool.Parse(comConfig.ToString()); + var comConfig = AppContext.GetData("System.Runtime.InteropServices.BuiltInComInterop.IsSupported") as string; + return comConfig == null || bool.Parse(comConfig); } [DllImport("UIAutomationCore.dll", EntryPoint = "UiaLookupId", CharSet = CharSet.Unicode)] diff --git a/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs index 88b907aeec..7eb457480b 100644 --- a/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs +++ b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs @@ -7,8 +7,8 @@ namespace Avalonia.Win32.Interop internal class TaskBarList { private static IntPtr s_taskBarList; - private static HrInit s_hrInitDelegate; - private static MarkFullscreenWindow s_markFullscreenWindowDelegate; + private static HrInit? s_hrInitDelegate; + private static MarkFullscreenWindow? s_markFullscreenWindowDelegate; /// /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc @@ -28,10 +28,7 @@ namespace Avalonia.Win32.Interop { var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); - if (s_hrInitDelegate is null) - { - s_hrInitDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); - } + s_hrInitDelegate ??= Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK) { @@ -44,10 +41,8 @@ namespace Avalonia.Win32.Interop { var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); - if (s_markFullscreenWindowDelegate is null) - { - s_markFullscreenWindowDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); - } + s_markFullscreenWindowDelegate ??= + Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen); } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 672f3a781a..1dd2ef20c7 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1,14 +1,12 @@ +#nullable enable + using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Text; - -using Avalonia.MicroCom; using MicroCom.Runtime; -using Avalonia.Win32.Win32Com; // ReSharper disable InconsistentNaming #pragma warning disable 169, 649 @@ -1092,7 +1090,7 @@ namespace Avalonia.Win32.Interop public const int SizeOf_BITMAPINFOHEADER = 40; [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)] - unsafe internal static extern int GetMouseMovePointsEx( + internal static extern int GetMouseMovePointsEx( uint cbSize, MOUSEMOVEPOINT* pointsIn, MOUSEMOVEPOINT* pointsBufferOut, int nBufPoints, uint resolution); @@ -1167,7 +1165,7 @@ namespace Avalonia.Win32.Interop public static extern IntPtr CreateWindowEx( int dwExStyle, uint lpClassName, - string lpWindowName, + string? lpWindowName, uint dwStyle, int x, int y, @@ -1218,7 +1216,7 @@ namespace Avalonia.Win32.Interop public static extern int GetMessageTime(); [DllImport("kernel32.dll")] - public static extern IntPtr GetModuleHandle(string lpModuleName); + public static extern IntPtr GetModuleHandle(string? lpModuleName); [DllImport("user32.dll")] public static extern int GetSystemMetrics(SystemMetric smIndex); @@ -1255,7 +1253,7 @@ namespace Avalonia.Win32.Interop } else { - return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr((uint)value)).ToInt32(); + return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr(value)).ToInt32(); } } @@ -1421,7 +1419,7 @@ namespace Avalonia.Win32.Interop public static extern bool UnregisterClass(string lpClassName, IntPtr hInstance); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetWindowTextW")] - public static extern bool SetWindowText(IntPtr hwnd, string lpString); + public static extern bool SetWindowText(IntPtr hwnd, string? lpString); public enum ClassLongIndex : int { @@ -1482,7 +1480,7 @@ namespace Avalonia.Win32.Interop internal static extern int CoCreateInstance(ref Guid clsid, IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter); - internal unsafe static T CreateInstance(ref Guid clsid, ref Guid iid) where T : IUnknown + internal static T CreateInstance(ref Guid clsid, ref Guid iid) where T : IUnknown { var hresult = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out IntPtr pUnk); if (hresult != 0) @@ -1490,7 +1488,7 @@ namespace Avalonia.Win32.Interop throw new COMException("CreateInstance", hresult); } using var unk = MicroComRuntime.CreateProxyFor(pUnk, true); - return MicroComRuntime.QueryInterface(unk); + return unk.QueryInterface(); } [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] @@ -1581,7 +1579,7 @@ namespace Avalonia.Win32.Interop public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi); [DllImport("user32")] - public static extern unsafe bool GetTouchInputInfo( + public static extern bool GetTouchInputInfo( IntPtr hTouchInput, uint cInputs, TOUCHINPUT* pInputs, @@ -1680,7 +1678,7 @@ namespace Avalonia.Win32.Interop public static extern IntPtr GlobalSize(IntPtr hGlobal); [DllImport("shell32.dll", BestFitMapping = false, CharSet = CharSet.Auto)] - public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder lpszFile, int cch); + public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder? lpszFile, int cch); [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] internal static extern void DoDragDrop(IntPtr dataObject, IntPtr dropSource, int allowedEffects, [Out] out int finalEffect); @@ -1734,7 +1732,7 @@ namespace Avalonia.Win32.Interop public DWM_BLURBEHIND(bool enabled) { - fEnable = enabled ? true : false; + fEnable = enabled; hRgnBlur = IntPtr.Zero; fTransitionOnMaximized = false; dwFlags = DWM_BB.Enable; @@ -1855,20 +1853,22 @@ namespace Avalonia.Win32.Interop [DllImport("imm32.dll", SetLastError = false, CharSet = CharSet.Unicode)] public static extern int ImmGetCompositionString(IntPtr hIMC, GCS dwIndex, [Out, Optional] IntPtr lpBuf, uint dwBufLen); - public static string ImmGetCompositionString(IntPtr hIMC, GCS dwIndex) + public static string? ImmGetCompositionString(IntPtr hIMC, GCS dwIndex) { int bufferLength = ImmGetCompositionString(hIMC, dwIndex, IntPtr.Zero, 0); if (bufferLength > 0) { - var buffer = new byte[bufferLength]; + var buffer = bufferLength <= 64 ? stackalloc byte[bufferLength] : new byte[bufferLength]; - fixed(byte* bufferPtr = buffer) + fixed (byte* bufferPtr = buffer) { - var error = ImmGetCompositionString(hIMC, dwIndex, (IntPtr)bufferPtr, (uint)bufferLength); - - return Marshal.PtrToStringUni((IntPtr)bufferPtr); - } + var result = ImmGetCompositionString(hIMC, dwIndex, (IntPtr)bufferPtr, (uint)bufferLength); + if (result >= 0) + { + return Marshal.PtrToStringUni((IntPtr)bufferPtr); + } + } } return null; @@ -2380,7 +2380,7 @@ namespace Avalonia.Win32.Interop } } - [StructLayoutAttribute(LayoutKind.Sequential)] + [StructLayout(LayoutKind.Sequential)] internal struct _DROPFILES { public Int32 pFiles; @@ -2390,7 +2390,7 @@ namespace Avalonia.Win32.Interop public bool fWide; } - [StructLayoutAttribute(LayoutKind.Sequential)] + [StructLayout(LayoutKind.Sequential)] internal struct STGMEDIUM { public TYMED tymed; @@ -2398,7 +2398,7 @@ namespace Avalonia.Win32.Interop public IntPtr pUnkForRelease; } - [StructLayoutAttribute(LayoutKind.Sequential)] + [StructLayout(LayoutKind.Sequential)] internal struct FORMATETC { public ushort cfFormat; @@ -2507,14 +2507,14 @@ namespace Avalonia.Win32.Interop public int uCallbackMessage; public IntPtr hIcon; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] - public string szTip; + public string? szTip; public int dwState = 0; public int dwStateMask = 0; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] - public string szInfo; + public string? szInfo; public int uTimeoutOrVersion; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] - public string szInfoTitle; + public string? szInfoTitle; public NIIF dwInfoFlags; } } diff --git a/src/Windows/Avalonia.Win32/NonPumpingSyncContext.cs b/src/Windows/Avalonia.Win32/NonPumpingSyncContext.cs index f295dd7394..0c02f643b2 100644 --- a/src/Windows/Avalonia.Win32/NonPumpingSyncContext.cs +++ b/src/Windows/Avalonia.Win32/NonPumpingSyncContext.cs @@ -1,7 +1,6 @@ using System; using System.Runtime.ConstrainedExecution; using System.Threading; -using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Interop; @@ -18,8 +17,8 @@ namespace Avalonia.Win32 SetSynchronizationContext(this); } - public override void Post(SendOrPostCallback d, object state) => _inner.Post(d, state); - public override void Send(SendOrPostCallback d, object state) => _inner.Send(d, state); + public override void Post(SendOrPostCallback d, object? state) => _inner.Post(d, state); + public override void Send(SendOrPostCallback d, object? state) => _inner.Send(d, state); #if !NET6_0_OR_GREATER [PrePrepareMethod] @@ -32,23 +31,15 @@ namespace Avalonia.Win32 public void Dispose() => SetSynchronizationContext(_inner); - public static IDisposable Use() + public static IDisposable? Use() { var current = Current; - if (current == null) - { - if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) - return null; - } - if (current is NonPumpingSyncContext) - return null; - - return new NonPumpingSyncContext(current); + return current is null or NonPumpingSyncContext ? null : new NonPumpingSyncContext(current); } internal class HelperImpl : NonPumpingLockHelper.IHelperImpl { - IDisposable NonPumpingLockHelper.IHelperImpl.Use() => NonPumpingSyncContext.Use(); + IDisposable? NonPumpingLockHelper.IHelperImpl.Use() => NonPumpingSyncContext.Use(); } } } diff --git a/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs index 9de105a3d5..7253b3c0ed 100644 --- a/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs +++ b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs @@ -1,17 +1,18 @@ using System; using System.ComponentModel; using System.Runtime.InteropServices; -using Avalonia.Platform; using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class OffscreenParentWindow + internal class OffscreenParentWindow { public static IntPtr Handle { get; } = CreateParentWindow(); - private static UnmanagedMethods.WndProc s_wndProcDelegate; + + private static UnmanagedMethods.WndProc? s_wndProcDelegate; + private static IntPtr CreateParentWindow() { - s_wndProcDelegate = new UnmanagedMethods.WndProc(ParentWndProc); + s_wndProcDelegate = ParentWndProc; var wndClassEx = new UnmanagedMethods.WNDCLASSEX { diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs index e41423a334..897033e835 100644 --- a/src/Windows/Avalonia.Win32/OleContext.cs +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -11,18 +11,16 @@ namespace Avalonia.Win32 { internal class OleContext { - private static OleContext s_current; + private static OleContext? s_current; - internal static OleContext Current + internal static OleContext? Current { get { if (!IsValidOleThread()) return null; - if (s_current == null) - s_current = new OleContext(); - return s_current; + return s_current ??= new OleContext(); } } @@ -41,7 +39,7 @@ namespace Avalonia.Win32 Thread.CurrentThread.GetApartmentState() == ApartmentState.STA; } - internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target) + internal bool RegisterDragDrop(IPlatformHandle? hwnd, IDropTarget? target) { if (hwnd?.HandleDescriptor != "HWND" || target == null) { @@ -52,7 +50,7 @@ namespace Avalonia.Win32 return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, trgPtr) == UnmanagedMethods.HRESULT.S_OK; } - internal bool UnregisterDragDrop(IPlatformHandle hwnd) + internal bool UnregisterDragDrop(IPlatformHandle? hwnd) { if (hwnd?.HandleDescriptor != "HWND") { diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index 79b55e4f77..247d0340c3 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -8,7 +8,6 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; -using Avalonia.MicroCom; using Avalonia.Utilities; using Avalonia.Win32.Interop; using MicroCom.Runtime; @@ -35,23 +34,23 @@ namespace Avalonia.Win32 return GetDataFormatsCore().Distinct(); } - public string GetText() + public string? GetText() { - return (string)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT); + return (string?)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT); } - public IEnumerable GetFileNames() + public IEnumerable? GetFileNames() { - return (IEnumerable)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT); + return (IEnumerable?)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT); } - public object Get(string dataFormat) + public object? Get(string dataFormat) { return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT); } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We still use BinaryFormatter for WinForms dragndrop compatability")] - private unsafe object GetDataFromOleHGLOBAL(string format, DVASPECT aspect) + private unsafe object? GetDataFromOleHGLOBAL(string format, DVASPECT aspect) { var formatEtc = new Interop.FORMATETC(); formatEtc.cfFormat = ClipboardFormats.GetFormat(format); @@ -100,7 +99,7 @@ namespace Avalonia.Win32 private static IEnumerable ReadFileNamesFromHGlobal(IntPtr hGlobal) { - List files = new List(); + var files = new List(); int fileCount = UnmanagedMethods.DragQueryFile(hGlobal, -1, null, 0); if (fileCount > 0) { @@ -118,7 +117,7 @@ namespace Avalonia.Win32 return files; } - private static string ReadStringFromHGlobal(IntPtr hGlobal) + private static string? ReadStringFromHGlobal(IntPtr hGlobal) { IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); try diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 8bbdfe5fd9..a81652ffc2 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -1,5 +1,5 @@ using System; - +using System.Diagnostics.CodeAnalysis; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.MicroCom; @@ -13,16 +13,16 @@ namespace Avalonia.Win32 internal class OleDropTarget : CallbackBase, Win32Com.IDropTarget { private readonly IInputRoot _target; - private readonly ITopLevelImpl _tl; + private readonly ITopLevelImpl _topLevel; private readonly IDragDropDevice _dragDevice; - private IDataObject _currentDrag = null; + private IDataObject? _currentDrag; - public OleDropTarget(ITopLevelImpl tl, IInputRoot target) + public OleDropTarget(ITopLevelImpl topLevel, IInputRoot target, IDragDropDevice dragDevice) { - _dragDevice = AvaloniaLocator.Current.GetService(); - _tl = tl; + _topLevel = topLevel; _target = target; + _dragDevice = dragDevice; } public static DropEffect ConvertDropEffect(DragDropEffects operation) @@ -71,10 +71,11 @@ namespace Avalonia.Win32 unsafe void Win32Com.IDropTarget.DragEnter(Win32Com.IDataObject pDataObj, int grfKeyState, UnmanagedMethods.POINT pt, DropEffect* pdwEffect) { - var dispatch = _tl?.Input; + var dispatch = _topLevel.Input; if (dispatch == null) { *pdwEffect= (int)DropEffect.None; + return; } SetDataObject(pDataObj); @@ -94,10 +95,11 @@ namespace Avalonia.Win32 unsafe void Win32Com.IDropTarget.DragOver(int grfKeyState, UnmanagedMethods.POINT pt, DropEffect* pdwEffect) { - var dispatch = _tl?.Input; - if (dispatch == null) + var dispatch = _topLevel.Input; + if (dispatch == null || _currentDrag is null) { *pdwEffect = (int)DropEffect.None; + return; } var args = new RawDragEvent( @@ -115,14 +117,20 @@ namespace Avalonia.Win32 void Win32Com.IDropTarget.DragLeave() { + var dispatch = _topLevel.Input; + if (dispatch == null || _currentDrag is null) + { + return; + } + try { - _tl?.Input(new RawDragEvent( + dispatch(new RawDragEvent( _dragDevice, RawDragEventType.DragLeave, _target, default, - null, + _currentDrag, DragDropEffects.None, RawInputModifiers.None )); @@ -137,10 +145,11 @@ namespace Avalonia.Win32 { try { - var dispatch = _tl?.Input; + var dispatch = _topLevel.Input; if (dispatch == null) { *pdwEffect = (int)DropEffect.None; + return; } SetDataObject(pDataObj); @@ -163,6 +172,7 @@ namespace Avalonia.Win32 } } + [MemberNotNull(nameof(_currentDrag))] private void SetDataObject(Win32Com.IDataObject pDataObj) { var newDrag = GetAvaloniaObjectFromCOM(pDataObj); @@ -178,9 +188,9 @@ namespace Avalonia.Win32 // OleDataObject keeps COM reference, so it should be disposed. if (_currentDrag is OleDataObject oleDragSource) { - oleDragSource?.Dispose(); - _currentDrag = null; + oleDragSource.Dispose(); } + _currentDrag = null; } private Point GetDragLocation(UnmanagedMethods.POINT dragPoint) @@ -194,7 +204,7 @@ namespace Avalonia.Win32 ReleaseDataObject(); } - public static unsafe IDataObject GetAvaloniaObjectFromCOM(Win32Com.IDataObject pDataObj) + public static IDataObject GetAvaloniaObjectFromCOM(Win32Com.IDataObject pDataObj) { if (pDataObj is null) { @@ -205,8 +215,7 @@ namespace Avalonia.Win32 return disposableDataObject; } - var dataObject = MicroComRuntime.TryUnwrapManagedObject(pDataObj) as DataObject; - if (dataObject is not null) + if (MicroComRuntime.TryUnwrapManagedObject(pDataObj) is DataObject dataObject) { return dataObject; } diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs index 6c951010c8..ffdd4fdfd6 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs @@ -14,7 +14,7 @@ internal class AngleD3DTextureFeature : IGlPlatformSurfaceRenderTargetFactory Display: AngleWin32EglDisplay { PlatformApi: AngleOptions.PlatformApi.DirectX11 } } && surface is IDirect3D11TexturePlatformSurface; - class RenderTargetWrapper : EglPlatformSurfaceRenderTargetBase + private class RenderTargetWrapper : EglPlatformSurfaceRenderTargetBase { private readonly AngleWin32EglDisplay _angle; private readonly IDirect3D11TextureRenderTarget _target; @@ -31,8 +31,8 @@ internal class AngleD3DTextureFeature : IGlPlatformSurfaceRenderTargetFactory { var success = false; var contextLock = Context.EnsureCurrent(); - IDirect3D11TextureRenderTargetRenderSession session = null; - EglSurface surface = null; + IDirect3D11TextureRenderTargetRenderSession? session = null; + EglSurface? surface = null; try { try diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs index 4e00bc2c72..2fd2e0f33c 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs @@ -22,7 +22,7 @@ namespace Avalonia.OpenGL.Angle } [GetProcAddress("eglCreateDeviceANGLE", true)] - public partial IntPtr CreateDeviceANGLE(int deviceType, IntPtr nativeDevice, int[] attribs); + public partial IntPtr CreateDeviceANGLE(int deviceType, IntPtr nativeDevice, int[]? attribs); [GetProcAddress("eglReleaseDeviceANGLE", true)] public partial void ReleaseDeviceANGLE(IntPtr device); diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs index b0fa76fda2..6bbfde35e1 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs @@ -1,8 +1,5 @@ -#nullable enable using System; -using System.Threading; using Avalonia.OpenGL; -using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Win32.DirectX; @@ -15,9 +12,12 @@ namespace Avalonia.Win32.OpenGl.Angle; internal class AngleExternalMemoryD3D11Texture2D : IGlExternalImageTexture { private readonly EglContext _context; - private ID3D11Texture2D _texture2D; - private EglSurface _eglSurface; - private IDXGIKeyedMutex _mutex; + private ID3D11Texture2D? _texture2D; + private EglSurface? _eglSurface; + private IDXGIKeyedMutex? _mutex; + + private IDXGIKeyedMutex Mutex + => _mutex ?? throw new ObjectDisposedException(nameof(AngleExternalMemoryD3D11Texture2D)); public unsafe AngleExternalMemoryD3D11Texture2D(EglContext context, ID3D11Texture2D texture2D, PlatformGraphicsExternalImageProperties props) { @@ -40,14 +40,14 @@ internal class AngleExternalMemoryD3D11Texture2D : IGlExternalImageTexture int temp = 0; gl.GenTextures(1, &temp); TextureId = temp; - gl.BindTexture(GlConsts.GL_TEXTURE_2D, TextureId); + gl.BindTexture(GL_TEXTURE_2D, TextureId); if (_context.Display.EglInterface.BindTexImage(_context.Display.Handle, _eglSurface.DangerousGetHandle(), EGL_BACK_BUFFER) == 0) throw OpenGlException.GetFormattedException("eglBindTexImage", _context.Display.EglInterface); } - + public void Dispose() { @@ -56,17 +56,15 @@ internal class AngleExternalMemoryD3D11Texture2D : IGlExternalImageTexture _context.GlInterface.DeleteTexture(TextureId); TextureId = 0; _eglSurface?.Dispose(); - _eglSurface = null!; + _eglSurface = null; _texture2D?.Dispose(); - _texture2D = null!; + _texture2D = null; _mutex?.Dispose(); - _mutex = null!; + _mutex = null; } - - public void AcquireKeyedMutex(uint key) => _mutex.AcquireSync(key, int.MaxValue); - - public void ReleaseKeyedMutex(uint key) => _mutex.ReleaseSync(key); + public void AcquireKeyedMutex(uint key) => Mutex.AcquireSync(key, int.MaxValue); + public void ReleaseKeyedMutex(uint key) => Mutex.ReleaseSync(key); public int TextureId { get; private set; } public int InternalFormat { get; } @@ -75,7 +73,7 @@ internal class AngleExternalMemoryD3D11Texture2D : IGlExternalImageTexture internal class AngleExternalMemoryD3D11ExportedTexture2D : AngleExternalMemoryD3D11Texture2D, IGlExportableExternalImageTexture { - static IPlatformHandle GetHandle(ID3D11Texture2D texture2D) + private static IPlatformHandle GetHandle(ID3D11Texture2D texture2D) { using var resource = texture2D.QueryInterface(); return new PlatformHandle(resource.SharedHandle, diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs index a7b52e953e..cad43e5d45 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; -using Avalonia.Controls.Documents; using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; @@ -93,8 +91,8 @@ internal class AngleExternalObjectsFeature : IGlContextExternalObjectsFeature, I return default; } - public byte[] DeviceLuid { get; } - public byte[] DeviceUuid { get; } + public byte[]? DeviceLuid { get; } + public byte[]? DeviceUuid => null; public void Dispose() { diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs index 53bf2fb8b1..770a27386f 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs @@ -1,4 +1,3 @@ -#nullable enable annotations using System; using System.Collections.Generic; using System.ComponentModel; @@ -97,7 +96,7 @@ namespace Avalonia.Win32.OpenGl.Angle DirectXUnmanagedMethods.D3D11CreateDevice(chosenAdapter?.GetNativeIntPtr() ?? IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN, IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, - 7, out pD3dDevice, out var featureLevel, null); + 7, out pD3dDevice, out _, null); if (pD3dDevice == IntPtr.Zero) diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs index a4d1ea457a..8ee6d286d1 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Logging; using Avalonia.OpenGL; @@ -12,11 +13,14 @@ namespace Avalonia.Win32.OpenGl.Angle; internal class AngleWin32PlatformGraphics : IPlatformGraphics, IPlatformGraphicsOpenGlContextFactory { private readonly Win32AngleEglInterface _egl; - private AngleWin32EglDisplay _sharedDisplay; - private EglContext _sharedContext; - public bool UsesSharedContext => PlatformApi == AngleOptions.PlatformApi.DirectX9; + private readonly AngleWin32EglDisplay? _sharedDisplay; + private EglContext? _sharedContext; + + [MemberNotNullWhen(true, nameof(_sharedDisplay))] + public bool UsesSharedContext => _sharedDisplay is not null; + + public AngleOptions.PlatformApi PlatformApi { get; } - public AngleOptions.PlatformApi PlatformApi { get; } = AngleOptions.PlatformApi.DirectX11; public IPlatformGraphicsContext CreateContext() { if (UsesSharedContext) @@ -72,7 +76,7 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics, IPlatformGraphics } - public static AngleWin32PlatformGraphics TryCreate(AngleOptions options) + public static AngleWin32PlatformGraphics? TryCreate(AngleOptions? options) { Win32AngleEglInterface egl; try @@ -109,7 +113,7 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics, IPlatformGraphics } else { - AngleWin32EglDisplay sharedDisplay = null; + AngleWin32EglDisplay? sharedDisplay = null; try { sharedDisplay = AngleWin32EglDisplay.CreateD3D9Display(egl); diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index b4002d8bc7..999818d709 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -7,22 +7,22 @@ using Avalonia.Platform; using Avalonia.Reactive; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; -using static Avalonia.Win32.OpenGl.WglConsts; namespace Avalonia.Win32.OpenGl { - class WglContext : IGlContext + internal class WglContext : IGlContext { - private object _lock = new object(); - private readonly WglContext _sharedWith; + private readonly object _lock = new(); + private readonly WglContext? _sharedWith; private readonly IntPtr _context; private readonly IntPtr _hWnd; private readonly IntPtr _dc; private readonly int _pixelFormat; private readonly PixelFormatDescriptor _formatDescriptor; + public IntPtr Handle => _context; - public WglContext(WglContext sharedWith, GlVersion version, IntPtr context, IntPtr hWnd, IntPtr dc, int pixelFormat, + public WglContext(WglContext? sharedWith, GlVersion version, IntPtr context, IntPtr hWnd, IntPtr dc, int pixelFormat, PixelFormatDescriptor formatDescriptor) { Version = version; @@ -54,7 +54,7 @@ namespace Avalonia.Win32.OpenGl public GlVersion Version { get; } public GlInterface GlInterface { get; } - public int SampleCount { get; } + public int SampleCount => 0; public int StencilSize { get; } private bool IsCurrent => wglGetCurrentContext() == _context && wglGetCurrentDC() == _dc; @@ -97,12 +97,12 @@ namespace Avalonia.Win32.OpenGl } public bool CanCreateSharedContext => true; - public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) + public IGlContext? CreateSharedContext(IEnumerable? preferredVersions = null) { var versions = preferredVersions?.Append(Version).ToArray() ?? new[] { Version }; return WglDisplay.CreateContext(versions, _sharedWith ?? this); } - public object TryGetFeature(Type featureType) => null; + public object? TryGetFeature(Type featureType) => null; } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index 9a1963a97a..e207bc23ab 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.OpenGL; using Avalonia.Threading; @@ -19,29 +20,31 @@ namespace Avalonia.Win32.OpenGl private static int _defaultPixelFormat; public static IntPtr OpenGl32Handle = LoadLibrary("opengl32"); - private delegate bool WglChoosePixelFormatARBDelegate(IntPtr hdc, int[] piAttribIList, float[] pfAttribFList, + private delegate bool WglChoosePixelFormatARBDelegate(IntPtr hdc, int[]? piAttribIList, float[]? pfAttribFList, int nMaxFormats, int[] piFormats, out int nNumFormats); - private static WglChoosePixelFormatARBDelegate WglChoosePixelFormatArb; + private static WglChoosePixelFormatARBDelegate? s_wglChoosePixelFormatArb; - private delegate IntPtr WglCreateContextAttribsARBDelegate(IntPtr hDC, IntPtr hShareContext, int[] attribList); + private delegate IntPtr WglCreateContextAttribsARBDelegate(IntPtr hDC, IntPtr hShareContext, int[]? attribList); - private static WglCreateContextAttribsARBDelegate WglCreateContextAttribsArb; + private static WglCreateContextAttribsARBDelegate? s_wglCreateContextAttribsArb; private delegate void GlDebugMessageCallbackDelegate(IntPtr callback, IntPtr userParam); - private static GlDebugMessageCallbackDelegate GlDebugMessageCallback; + private static GlDebugMessageCallbackDelegate? s_glDebugMessageCallback; private delegate void DebugCallbackDelegate(int source, int type, int id, int severity, int len, IntPtr message, IntPtr userParam); - - static bool Initialize() - { - if (!_initialized.HasValue) - _initialized = InitializeCore(); - return _initialized.Value; - } - static bool InitializeCore() + + [MemberNotNullWhen(true, nameof(s_wglChoosePixelFormatArb))] + [MemberNotNullWhen(true, nameof(s_wglCreateContextAttribsArb))] + [MemberNotNullWhen(true, nameof(s_glDebugMessageCallback))] + private static bool Initialize() => _initialized ??= InitializeCore(); + + [MemberNotNullWhen(true, nameof(s_wglChoosePixelFormatArb))] + [MemberNotNullWhen(true, nameof(s_wglCreateContextAttribsArb))] + [MemberNotNullWhen(true, nameof(s_glDebugMessageCallback))] + private static bool InitializeCore() { Dispatcher.UIThread.VerifyAccess(); _bootstrapWindow = WglGdiResourceManager.CreateOffscreenWindow(); @@ -64,20 +67,20 @@ namespace Avalonia.Win32.OpenGl return false; wglMakeCurrent(_bootstrapDc, _bootstrapContext); - WglCreateContextAttribsArb = Marshal.GetDelegateForFunctionPointer( + s_wglCreateContextAttribsArb = Marshal.GetDelegateForFunctionPointer( wglGetProcAddress("wglCreateContextAttribsARB")); - WglChoosePixelFormatArb = + s_wglChoosePixelFormatArb = Marshal.GetDelegateForFunctionPointer( wglGetProcAddress("wglChoosePixelFormatARB")); - GlDebugMessageCallback = + s_glDebugMessageCallback = Marshal.GetDelegateForFunctionPointer( wglGetProcAddress("glDebugMessageCallback")); var formats = new int[1]; - WglChoosePixelFormatArb(_bootstrapDc, new int[] + s_wglChoosePixelFormatArb(_bootstrapDc, new int[] { WGL_DRAW_TO_WINDOW_ARB, 1, WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, @@ -106,12 +109,12 @@ namespace Avalonia.Win32.OpenGl Console.Error.WriteLine(err); } - public static WglContext CreateContext(GlVersion[] versions, IGlContext share) + public static WglContext? CreateContext(GlVersion[] versions, IGlContext? share) { if (!Initialize()) return null; - var shareContext = (WglContext)share; + var shareContext = share as WglContext; using (new WglRestoreContext(_bootstrapDc, _bootstrapContext, null)) { @@ -125,7 +128,7 @@ namespace Avalonia.Win32.OpenGl IntPtr context; using (shareContext?.Lock()) { - context = WglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, + context = s_wglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, new[] { // major @@ -142,7 +145,7 @@ namespace Avalonia.Win32.OpenGl } using(new WglRestoreContext(dc, context, null)) - GlDebugMessageCallback(Marshal.GetFunctionPointerForDelegate(_debugCallback), IntPtr.Zero); + s_glDebugMessageCallback(Marshal.GetFunctionPointerForDelegate(_debugCallback), IntPtr.Zero); if (context != IntPtr.Zero) return new WglContext(shareContext, version, context, window, dc, _defaultPixelFormat, _defaultPfd); diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs index ed40488c70..86143d078d 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs @@ -17,45 +17,69 @@ namespace Avalonia.Win32.OpenGl; internal class WglGdiResourceManager { - class GetDCOp + private class GetDCOp { - public IntPtr Window; - public TaskCompletionSource Result; + public readonly IntPtr Window; + public readonly TaskCompletionSource Result; + + public GetDCOp(IntPtr window, TaskCompletionSource result) + { + Window = window; + Result = result; + } } - class ReleaseDCOp + private class ReleaseDCOp { - public IntPtr Window; - public IntPtr DC; - public TaskCompletionSource Result; + public readonly IntPtr Window; + public readonly IntPtr DC; + public readonly TaskCompletionSource Result; + + public ReleaseDCOp(IntPtr window, IntPtr dc, TaskCompletionSource result) + { + Window = window; + DC = dc; + Result = result; + } } - - class CreateWindowOp + + private class CreateWindowOp { - public TaskCompletionSource Result; + public readonly TaskCompletionSource Result; + + public CreateWindowOp(TaskCompletionSource result) + { + Result = result; + } } - class DestroyWindowOp + private class DestroyWindowOp { - public IntPtr Window; - public TaskCompletionSource Result; + public readonly IntPtr Window; + public readonly TaskCompletionSource Result; + + public DestroyWindowOp(IntPtr window, TaskCompletionSource result) + { + Window = window; + Result = result; + } } - private static readonly Queue s_Queue = new(); - private static readonly AutoResetEvent s_Event = new(false); - private static readonly ushort s_WindowClass; + private static readonly Queue s_queue = new(); + private static readonly AutoResetEvent s_event = new(false); + private static readonly ushort s_windowClass; private static readonly UnmanagedMethods.WndProc s_wndProcDelegate = WndProc; - static void Worker() + private static void Worker() { while (true) { - s_Event.WaitOne(); - lock (s_Queue) + s_event.WaitOne(); + lock (s_queue) { - if(s_Queue.Count == 0) + if(s_queue.Count == 0) continue; - var job = s_Queue.Dequeue(); + var job = s_queue.Dequeue(); if (job is GetDCOp getDc) getDc.Result.TrySetResult(UnmanagedMethods.GetDC(getDc.Window)); else if (job is ReleaseDCOp releaseDc) @@ -66,7 +90,7 @@ internal class WglGdiResourceManager else if (job is CreateWindowOp createWindow) createWindow.Result.TrySetResult(UnmanagedMethods.CreateWindowEx( 0, - s_WindowClass, + s_windowClass, null, (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, 0, @@ -97,16 +121,15 @@ internal class WglGdiResourceManager style = (int)UnmanagedMethods.ClassStyles.CS_OWNDC }; - s_WindowClass = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + s_windowClass = UnmanagedMethods.RegisterClassEx(ref wndClassEx); var th = new Thread(Worker) { IsBackground = true, Name = "Win32 OpenGL HDC manager" }; // This makes CLR to automatically pump the event queue from WaitOne th.SetApartmentState(ApartmentState.STA); th.Start(); } - - - - static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + + + private static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } @@ -114,53 +137,36 @@ internal class WglGdiResourceManager public static IntPtr CreateOffscreenWindow() { var tcs = new TaskCompletionSource(); - lock(s_Queue) - s_Queue.Enqueue(new CreateWindowOp() - { - Result = tcs - }); - s_Event.Set(); + lock (s_queue) + s_queue.Enqueue(new CreateWindowOp(tcs)); + s_event.Set(); return tcs.Task.Result; } public static IntPtr GetDC(IntPtr hWnd) { var tcs = new TaskCompletionSource(); - lock(s_Queue) - s_Queue.Enqueue(new GetDCOp - { - Window = hWnd, - Result = tcs - }); - s_Event.Set(); + lock (s_queue) + s_queue.Enqueue(new GetDCOp(hWnd, tcs)); + s_event.Set(); return tcs.Task.Result; } public static void ReleaseDC(IntPtr hWnd, IntPtr hDC) { - var tcs = new TaskCompletionSource(); - lock(s_Queue) - s_Queue.Enqueue(new ReleaseDCOp() - { - Window = hWnd, - DC = hDC, - Result = tcs - }); - s_Event.Set(); + var tcs = new TaskCompletionSource(); + lock (s_queue) + s_queue.Enqueue(new ReleaseDCOp(hWnd, hDC, tcs)); + s_event.Set(); tcs.Task.Wait(); } public static void DestroyWindow(IntPtr hWnd) { - var tcs = new TaskCompletionSource(); - lock(s_Queue) - s_Queue.Enqueue(new DestroyWindowOp() - { - Window = hWnd, - Result = tcs - }); - s_Event.Set(); + var tcs = new TaskCompletionSource(); + lock (s_queue) + s_queue.Enqueue(new DestroyWindowOp(hWnd, tcs)); + s_event.Set(); tcs.Task.Wait(); - } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs index 39dd330d52..61b932e9c7 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -6,27 +6,31 @@ using Avalonia.Platform; namespace Avalonia.Win32.OpenGl { - class WglPlatformOpenGlInterface : IPlatformGraphics + internal class WglPlatformOpenGlInterface : IPlatformGraphics { public WglContext PrimaryContext { get; } public bool UsesSharedContext => false; IPlatformGraphicsContext IPlatformGraphics.CreateContext() => CreateContext(); public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); - - public IGlContext CreateContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, null); - private WglPlatformOpenGlInterface(WglContext primary) + public IGlContext CreateContext() + => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, null) + ?? throw new OpenGlException("Unable to create additional WGL context"); + + private WglPlatformOpenGlInterface(WglContext primary) { PrimaryContext = primary; } - public static WglPlatformOpenGlInterface TryCreate() + public static WglPlatformOpenGlInterface? TryCreate() { try { var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); - var primary = WglDisplay.CreateContext(opts.WglProfiles.ToArray(), null); - return new WglPlatformOpenGlInterface(primary); + if (WglDisplay.CreateContext(opts.WglProfiles.ToArray(), null) is { } primary) + { + return new WglPlatformOpenGlInterface(primary); + } } catch (Exception e) { diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs index b145ffbb37..ee0934af7d 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs @@ -8,11 +8,11 @@ namespace Avalonia.Win32.OpenGl { internal class WglRestoreContext : IDisposable { - private readonly object _monitor; + private readonly object? _monitor; private readonly IntPtr _oldDc; private readonly IntPtr _oldContext; - public WglRestoreContext(IntPtr gc, IntPtr context, object monitor, bool takeMonitor = true) + public WglRestoreContext(IntPtr gc, IntPtr context, object? monitor, bool takeMonitor = true) { _monitor = monitor; _oldDc = wglGetCurrentDC(); diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 3a56af12f0..75c1a2d564 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -5,9 +5,9 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class PopupImpl : WindowImpl, IPopupImpl + internal class PopupImpl : WindowImpl, IPopupImpl { - private readonly IWindowBaseImpl _parent; + private readonly IWindowBaseImpl? _parent; private bool _dropShadowHint = true; private Size? _maxAutoSize; @@ -92,7 +92,7 @@ namespace Avalonia.Win32 IntPtr.Zero); s_parentHandle = IntPtr.Zero; - PopupImpl.EnableBoxShadow(result, _dropShadowHint); + EnableBoxShadow(result, _dropShadowHint); return result; } @@ -113,7 +113,7 @@ namespace Avalonia.Win32 // This is needed because we are calling virtual methods from constructors // One fabulous design decision leads to another, I guess - static IWindowBaseImpl SaveParentHandle(IWindowBaseImpl parent) + private static IWindowBaseImpl SaveParentHandle(IWindowBaseImpl parent) { s_parentHandle = parent.Handle.Handle; return parent; @@ -126,7 +126,7 @@ namespace Avalonia.Win32 } - private PopupImpl(IWindowBaseImpl parent, bool dummy) : base() + private PopupImpl(IWindowBaseImpl parent, bool dummy) { _parent = parent; PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); @@ -159,10 +159,7 @@ namespace Avalonia.Win32 { _dropShadowHint = enabled; - if (Handle != null) - { - PopupImpl.EnableBoxShadow(Handle.Handle, enabled); - } + EnableBoxShadow(Handle.Handle, enabled); } public IPopupPositioner PopupPositioner { get; } diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index f3754cd58f..69431c7ff6 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Metadata; using Avalonia.Platform; -using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 @@ -11,12 +10,15 @@ namespace Avalonia.Win32 [Unstable] public class ScreenImpl : IScreenImpl { + private Screen[]? _allScreens; + + /// public int ScreenCount { get => GetSystemMetrics(SystemMetric.SM_CMONITORS); } - private Screen[] _allScreens; + /// public IReadOnlyList AllScreens { get @@ -38,7 +40,7 @@ namespace Avalonia.Win32 if (method != IntPtr.Zero) { GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); - dpi = (double)x; + dpi = x; } else { @@ -74,7 +76,8 @@ namespace Avalonia.Win32 _allScreens = null; } - public Screen ScreenFromWindow(IWindowBaseImpl window) + /// + public Screen? ScreenFromWindow(IWindowBaseImpl window) { var handle = window.Handle.Handle; @@ -83,7 +86,8 @@ namespace Avalonia.Win32 return FindScreenByHandle(monitor); } - public Screen ScreenFromPoint(PixelPoint point) + /// + public Screen? ScreenFromPoint(PixelPoint point) { var monitor = MonitorFromPoint(new POINT { @@ -94,7 +98,8 @@ namespace Avalonia.Win32 return FindScreenByHandle(monitor); } - public Screen ScreenFromRect(PixelRect rect) + /// + public Screen? ScreenFromRect(PixelRect rect) { var monitor = MonitorFromRect(new RECT { @@ -107,7 +112,7 @@ namespace Avalonia.Win32 return FindScreenByHandle(monitor); } - private Screen FindScreenByHandle(IntPtr handle) + private Screen? FindScreenByHandle(IntPtr handle) { return AllScreens.Cast().FirstOrDefault(m => m.Handle == handle); } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index fe920ff1a1..8a10c6d2e1 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -11,8 +11,6 @@ using Avalonia.Styling; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; -#nullable enable - namespace Avalonia.Win32 { [Unstable] @@ -25,9 +23,9 @@ namespace Avalonia.Win32 private IconImpl? _icon; private string? _tooltipText; private readonly Win32NativeToManagedMenuExporter _exporter; - private static readonly Dictionary s_trayIcons = new Dictionary(); + private static readonly Dictionary s_trayIcons = new(); private bool _disposedValue; - private static readonly uint WM_TASKBARCREATED = UnmanagedMethods.RegisterWindowMessage("TaskbarCreated"); + private static readonly uint WM_TASKBARCREATED = RegisterWindowMessage("TaskbarCreated"); public TrayIconImpl() { @@ -62,17 +60,20 @@ namespace Avalonia.Win32 } } + /// public void SetIcon(IWindowIconImpl? icon) { _icon = icon as IconImpl; UpdateIcon(); } + /// public void SetIsVisible(bool visible) { UpdateIcon(!visible); } + /// public void SetToolTipText(string? text) { _tooltipText = text; @@ -209,8 +210,11 @@ namespace Avalonia.Win32 private void MoveResize(PixelPoint position, Size size, double scaling) { - PlatformImpl!.Move(position); - PlatformImpl!.Resize(size, PlatformResizeReason.Layout); + if (PlatformImpl is { } platformImpl) + { + platformImpl.Move(position); + platformImpl.Resize(size, PlatformResizeReason.Layout); + } } protected override void ArrangeCore(Rect finalRect) @@ -221,7 +225,7 @@ namespace Avalonia.Win32 { Anchor = PopupAnchor.TopLeft, Gravity = PopupGravity.BottomRight, - AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.Scaling, new Size(1, 1)), + AnchorRectangle = new Rect(Position.ToPoint(Screens.Primary?.Scaling ?? 1.0), new Size(1, 1)), Size = finalRect.Size, ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY, }); @@ -247,18 +251,22 @@ namespace Avalonia.Win32 { get { - var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft; - var size = _hiddenWindow.Screens.Primary.Bounds.Size; - return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.Scaling, size.Height * _hiddenWindow.Screens.Primary.Scaling); + if (_hiddenWindow.Screens.Primary is { } screen) + { + var point = screen.Bounds.TopLeft; + var size = screen.Bounds.Size; + return new Rect(point.X, point.Y, size.Width * screen.Scaling, size.Height * screen.Scaling); + } + return default; } } public void MoveAndResize(Point devicePoint, Size virtualSize) { - _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.Scaling); + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, Scaling); } - public double Scaling => _hiddenWindow.Screens.Primary.Scaling; + public double Scaling => _hiddenWindow.Screens.Primary?.Scaling ?? 1.0; } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index ab6de1a027..c26ce5fd45 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,6 +1,4 @@ using Avalonia.OpenGL; -using Avalonia.OpenGL.Angle; -using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Win32.DirectX; using Avalonia.Win32.OpenGl; @@ -11,16 +9,19 @@ namespace Avalonia.Win32 { static class Win32GlManager { - - public static IPlatformGraphics Initialize() + public static IPlatformGraphics? Initialize() { var gl = InitializeCore(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + + if (gl is not null) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + } + return gl; } - static IPlatformGraphics InitializeCore() + private static IPlatformGraphics? InitializeCore() { var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); @@ -55,7 +56,5 @@ namespace Avalonia.Win32 return null; } - - } } diff --git a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs index fd05e780bf..620cd64e79 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs @@ -1,13 +1,13 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Controls.Platform; using Avalonia.Platform; -using Avalonia.VisualTree; using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class Win32NativeControlHost : INativeControlHostImpl + internal class Win32NativeControlHost : INativeControlHostImpl { private readonly bool _useLayeredWindow; public WindowImpl Window { get; } @@ -18,7 +18,7 @@ namespace Avalonia.Win32 Window = window; } - void AssertCompatible(IPlatformHandle handle) + private void AssertCompatible(IPlatformHandle handle) { if (!IsCompatibleWith(handle)) throw new ArgumentException($"Don't know what to do with {handle.HandleDescriptor}"); @@ -33,7 +33,7 @@ namespace Avalonia.Win32 public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) { var holder = new DumbWindow(_useLayeredWindow, Window.Handle.Handle); - Win32NativeControlAttachment attachment = null; + Win32NativeControlAttachment? attachment = null; try { var child = create(holder); @@ -46,7 +46,7 @@ namespace Avalonia.Win32 catch { attachment?.Dispose(); - holder?.Destroy(); + holder.Destroy(); throw; } } @@ -60,13 +60,13 @@ namespace Avalonia.Win32 public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == "HWND"; - class DumbWindow : IDisposable, INativeControlHostDestroyableControlHandle + private class DumbWindow : IDisposable, INativeControlHostDestroyableControlHandle { public IntPtr Handle { get;} public string HandleDescriptor => "HWND"; public void Destroy() => Dispose(); - UnmanagedMethods.WndProc _wndProcDelegate; + private readonly UnmanagedMethods.WndProc _wndProcDelegate; private readonly string _className; public DumbWindow(bool layered = false, IntPtr? parent = null) @@ -103,9 +103,7 @@ namespace Avalonia.Win32 UnmanagedMethods.LayeredWindowFlags.LWA_ALPHA); } - - - protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + private static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } @@ -128,11 +126,11 @@ namespace Avalonia.Win32 } } - class Win32NativeControlAttachment : INativeControlHostControlTopLevelAttachment + private class Win32NativeControlAttachment : INativeControlHostControlTopLevelAttachment { - private DumbWindow _holder; - private IPlatformHandle _child; - private Win32NativeControlHost _attachedTo; + private DumbWindow? _holder; + private IPlatformHandle? _child; + private Win32NativeControlHost? _attachedTo; public Win32NativeControlAttachment(DumbWindow holder, IPlatformHandle child) { @@ -142,7 +140,8 @@ namespace Avalonia.Win32 UnmanagedMethods.ShowWindow(child.Handle, UnmanagedMethods.ShowWindowCommand.Show); } - void CheckDisposed() + [MemberNotNull(nameof(_holder))] + private void CheckDisposed() { if (_holder == null) throw new ObjectDisposedException(nameof(Win32NativeControlAttachment)); @@ -158,13 +157,13 @@ namespace Avalonia.Win32 _attachedTo = null; } - public INativeControlHostImpl AttachedTo + public INativeControlHostImpl? AttachedTo { get => _attachedTo; set { CheckDisposed(); - _attachedTo = (Win32NativeControlHost) value; + _attachedTo = value as Win32NativeControlHost; if (_attachedTo == null) { UnmanagedMethods.ShowWindow(_holder.Handle, UnmanagedMethods.ShowWindowCommand.Hide); @@ -179,6 +178,7 @@ namespace Avalonia.Win32 public void HideWithSize(Size size) { + CheckDisposed(); UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, -100, -100, 1, 1, UnmanagedMethods.SetWindowPosFlags.SWP_HIDEWINDOW | @@ -198,8 +198,12 @@ namespace Avalonia.Win32 bounds *= _attachedTo.Window.RenderScaling; var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), Math.Max(1, (int)bounds.Height)); - - UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, pixelRect.Width, pixelRect.Height, true); + + if (_child is not null) + { + UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, pixelRect.Width, pixelRect.Height, true); + } + UnmanagedMethods.SetWindowPos(_holder.Handle, IntPtr.Zero, pixelRect.X, pixelRect.Y, pixelRect.Width, pixelRect.Height, UnmanagedMethods.SetWindowPosFlags.SWP_SHOWWINDOW diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index fdf36206d0..46cdf66653 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -1,12 +1,7 @@ -using System.Collections.Generic; -using Avalonia.Reactive; +using Avalonia.Reactive; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Media.Imaging; -using Avalonia.Utilities; - -#nullable enable namespace Avalonia.Win32 { @@ -19,7 +14,7 @@ namespace Avalonia.Win32 _nativeMenu = nativeMenu; } - private AvaloniaList Populate(NativeMenu nativeMenu) + private static AvaloniaList Populate(NativeMenu nativeMenu) { var result = new AvaloniaList(); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c34be9008a..4b327ed02a 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -17,12 +17,10 @@ using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; -using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia { -#nullable enable public static class Win32ApplicationExtensions { public static AppBuilder UseWin32(this AppBuilder builder) @@ -106,17 +104,19 @@ namespace Avalonia public IPlatformGraphics? CustomPlatformGraphics { get; set; } } } -#nullable restore namespace Avalonia.Win32 { public class Win32Platform : IPlatformThreadingInterface, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl { - private static readonly Win32Platform s_instance = new Win32Platform(); - private static Thread _uiThread; - private UnmanagedMethods.WndProc _wndProcDelegate; + private static readonly Win32Platform s_instance = new(); + private static Thread? s_uiThread; + private static Win32PlatformOptions? s_options; + private static Compositor? s_compositor; + + private WndProc? _wndProcDelegate; private IntPtr _hwnd; - private readonly List _delegates = new List(); + private readonly List _delegates = new(); public Win32Platform() { @@ -135,9 +135,12 @@ namespace Avalonia.Win32 public static Version WindowsVersion { get; } = RtlGetVersion(); internal static bool UseOverlayPopups => Options.OverlayPopups; - public static Win32PlatformOptions Options { get; private set; } - internal static Compositor Compositor { get; private set; } + public static Win32PlatformOptions Options + => s_options ?? throw new InvalidOperationException($"{nameof(Win32Platform)} hasn't been initialized"); + + internal static Compositor Compositor + => s_compositor ?? throw new InvalidOperationException($"{nameof(Win32Platform)} hasn't been initialized"); public static void Initialize() { @@ -146,7 +149,7 @@ namespace Avalonia.Win32 public static void Initialize(Win32PlatformOptions options) { - Options = options; + s_options = options; var renderTimer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) : new DefaultRenderTimer(60); AvaloniaLocator.CurrentMutable @@ -171,26 +174,24 @@ namespace Avalonia.Win32 .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - _uiThread = Thread.CurrentThread; + s_uiThread = Thread.CurrentThread; - var platformGraphics = options?.CustomPlatformGraphics + var platformGraphics = options.CustomPlatformGraphics ?? Win32GlManager.Initialize(); if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), platformGraphics); + s_compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), platformGraphics); } public bool HasMessages() { - UnmanagedMethods.MSG msg; - return PeekMessage(out msg, IntPtr.Zero, 0, 0, 0); + return PeekMessage(out _, IntPtr.Zero, 0, 0, 0); } public void ProcessMessage() { - if (GetMessage(out var msg, IntPtr.Zero, 0, 0) > -1) { TranslateMessage(ref msg); @@ -222,8 +223,7 @@ namespace Avalonia.Win32 public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action callback) { - UnmanagedMethods.TimerProc timerDelegate = - (hWnd, uMsg, nIDEvent, dwTime) => callback(); + TimerProc timerDelegate = (_, _, _, _) => callback(); IntPtr handle = SetTimer( IntPtr.Zero, @@ -253,11 +253,11 @@ namespace Avalonia.Win32 new IntPtr(SignalL)); } - public bool CurrentThreadIsLoopThread => _uiThread == Thread.CurrentThread; + public bool CurrentThreadIsLoopThread => s_uiThread == Thread.CurrentThread; - public event Action Signaled; + public event Action? Signaled; - public event EventHandler ShutdownRequested; + public event EventHandler? ShutdownRequested; [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")] private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) @@ -301,11 +301,11 @@ namespace Avalonia.Win32 private void CreateMessageWindow() { // Ensure that the delegate doesn't get garbage collected by storing it as a field. - _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); + _wndProcDelegate = WndProc; - UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX + WNDCLASSEX wndClassEx = new WNDCLASSEX { - cbSize = Marshal.SizeOf(), + cbSize = Marshal.SizeOf(), lpfnWndProc = _wndProcDelegate, hInstance = GetModuleHandle(null), lpszClassName = "AvaloniaMessageWindow " + Guid.NewGuid(), @@ -326,7 +326,7 @@ namespace Avalonia.Win32 } } - public ITrayIconImpl CreateTrayIcon () + public ITrayIconImpl CreateTrayIcon() { return new TrayIconImpl(); } diff --git a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs index 0310c4bfad..c5f7f17b32 100644 --- a/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs +++ b/src/Windows/Avalonia.Win32/Win32PlatformSettings.cs @@ -1,8 +1,6 @@ using System; -using System.Runtime.InteropServices; using Avalonia.Input; using Avalonia.Platform; -using Avalonia.Win32.Interop; using Avalonia.Win32.WinRT; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -10,14 +8,14 @@ namespace Avalonia.Win32; internal class Win32PlatformSettings : DefaultPlatformSettings { - private PlatformColorValues _lastColorValues; + private PlatformColorValues? _lastColorValues; public override Size GetTapSize(PointerType type) { return type switch { PointerType.Touch => new(10, 10), - _ => new(GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDRAG), GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDRAG)), + _ => new(GetSystemMetrics(SystemMetric.SM_CXDRAG), GetSystemMetrics(SystemMetric.SM_CYDRAG)), }; } @@ -26,7 +24,7 @@ internal class Win32PlatformSettings : DefaultPlatformSettings return type switch { PointerType.Touch => new(16, 16), - _ => new(GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK), GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDOUBLECLK)), + _ => new(GetSystemMetrics(SystemMetric.SM_CXDOUBLECLK), GetSystemMetrics(SystemMetric.SM_CYDOUBLECLK)), }; } diff --git a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs index 2da642bd19..2fd49c8b09 100644 --- a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs +++ b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs @@ -1,13 +1,10 @@ -#nullable enable - -using System; +using System; using System.Linq; using System.Collections.Generic; using System.IO; using System.ComponentModel; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Avalonia.MicroCom; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; using Avalonia.Win32.Interop; @@ -146,7 +143,7 @@ namespace Avalonia.Win32 } } - var showResult = frm.Show(_windowImpl.Handle!.Handle); + var showResult = frm.Show(_windowImpl.Handle.Handle); if ((uint)showResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED) { @@ -188,7 +185,7 @@ namespace Avalonia.Win32 var message = new Win32Exception(ex.HResult).Message; throw new COMException(message, ex); } - })!; + }); } @@ -220,7 +217,6 @@ namespace Avalonia.Win32 } var size = Marshal.SizeOf(); - var arr = new byte[size]; var resultArr = new byte[size * filters.Count]; for (int i = 0; i < filters.Count; i++) @@ -236,7 +232,7 @@ namespace Avalonia.Win32 { var filterStr = new UnmanagedMethods.COMDLG_FILTERSPEC { - pszName = filter.Name ?? string.Empty, + pszName = filter.Name, pszSpec = string.Join(";", filter.Patterns) }; diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs index 32f44842f8..1a922b4acd 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs @@ -1,14 +1,13 @@ using System; using System.Linq; using System.Runtime.InteropServices; -using Avalonia.MicroCom; using MicroCom.Runtime; namespace Avalonia.Win32.WinRT.Composition { - abstract class WinUIEffectBase : WinRTInspectable, IGraphicsEffect, IGraphicsEffectSource, IGraphicsEffectD2D1Interop + internal abstract class WinUIEffectBase : WinRTInspectable, IGraphicsEffect, IGraphicsEffectSource, IGraphicsEffectD2D1Interop { - private IGraphicsEffectSource[] _sources; + private IGraphicsEffectSource[]? _sources; public WinUIEffectBase(params IGraphicsEffectSource[] _sources) { @@ -32,7 +31,7 @@ namespace Avalonia.Win32.WinRT.Composition throw new COMException("Not supported", unchecked((int)0x80004001)); public abstract uint PropertyCount { get; } - public abstract IPropertyValue GetProperty(uint index); + public abstract IPropertyValue? GetProperty(uint index); public IGraphicsEffectSource GetSource(uint index) { @@ -53,14 +52,14 @@ namespace Avalonia.Win32.WinRT.Composition _sources = null; } } - - class WinUIGaussianBlurEffect : WinUIEffectBase + + internal class WinUIGaussianBlurEffect : WinUIEffectBase { public WinUIGaussianBlurEffect(IGraphicsEffectSource source) : base(source) { } - enum D2D1_GAUSSIANBLUR_OPTIMIZATION + private enum D2D1_GAUSSIANBLUR_OPTIMIZATION { D2D1_GAUSSIANBLUR_OPTIMIZATION_SPEED, D2D1_GAUSSIANBLUR_OPTIMIZATION_BALANCED, @@ -68,14 +67,14 @@ namespace Avalonia.Win32.WinRT.Composition D2D1_GAUSSIANBLUR_OPTIMIZATION_FORCE_DWORD }; - enum D2D1_BORDER_MODE + private enum D2D1_BORDER_MODE { D2D1_BORDER_MODE_SOFT, D2D1_BORDER_MODE_HARD, D2D1_BORDER_MODE_FORCE_DWORD }; - enum D2D1GaussianBlurProp + private enum D2D1GaussianBlurProp { D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, D2D1_GAUSSIANBLUR_PROP_OPTIMIZATION, @@ -87,7 +86,7 @@ namespace Avalonia.Win32.WinRT.Composition public override uint PropertyCount => 3; - public override IPropertyValue GetProperty(uint index) + public override IPropertyValue? GetProperty(uint index) { switch ((D2D1GaussianBlurProp)index) { @@ -105,14 +104,14 @@ namespace Avalonia.Win32.WinRT.Composition return null; } } - - class SaturationEffect : WinUIEffectBase + + internal class SaturationEffect : WinUIEffectBase { public SaturationEffect(IGraphicsEffectSource source) : base(source) { } - enum D2D1_SATURATION_PROP + private enum D2D1_SATURATION_PROP { D2D1_SATURATION_PROP_SATURATION, D2D1_SATURATION_PROP_FORCE_DWORD @@ -122,7 +121,7 @@ namespace Avalonia.Win32.WinRT.Composition public override uint PropertyCount => 1; - public override IPropertyValue GetProperty(uint index) + public override IPropertyValue? GetProperty(uint index) { switch ((D2D1_SATURATION_PROP)index) { diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs index 5be25a3d4b..2f22ba99f9 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Threading; using Avalonia.OpenGL.Egl; using Avalonia.Reactive; -using Avalonia.Win32.Interop; using MicroCom.Runtime; namespace Avalonia.Win32.WinRT.Composition; @@ -12,8 +11,8 @@ internal class WinUiCompositedWindow : IDisposable { public EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo WindowInfo { get; } private readonly WinUiCompositionShared _shared; - private readonly ICompositionRoundedRectangleGeometry _compositionRoundedRectangleGeometry; - private readonly IVisual _mica; + private readonly ICompositionRoundedRectangleGeometry? _compositionRoundedRectangleGeometry; + private readonly IVisual? _mica; private readonly IVisual _blur; private readonly IVisual _visual; private PixelSize _size; @@ -25,7 +24,7 @@ internal class WinUiCompositedWindow : IDisposable lock (_shared.SyncRoot) { _compositionRoundedRectangleGeometry?.Dispose(); - _blur?.Dispose(); + _blur.Dispose(); _mica?.Dispose(); _visual.Dispose(); _surfaceBrush.Dispose(); diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs index 9ee024f9ad..95ef338f08 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs @@ -1,15 +1,8 @@ using System; -using System.Runtime.InteropServices; -using Avalonia.MicroCom; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; -using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; -using Avalonia.Utilities; using Avalonia.Win32.DirectX; using Avalonia.Win32.Interop; -using Avalonia.Win32.OpenGl.Angle; using MicroCom.Runtime; namespace Avalonia.Win32.WinRT.Composition @@ -18,7 +11,7 @@ namespace Avalonia.Win32.WinRT.Composition { private readonly WinUiCompositionShared _shared; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - private WinUiCompositedWindow _window; + private WinUiCompositedWindow? _window; private BlurEffect _blurEffect; public WinUiCompositedWindowSurface(WinUiCompositionShared shared, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) @@ -52,6 +45,8 @@ namespace Avalonia.Win32.WinRT.Composition internal class WinUiCompositedWindowRenderTarget : IDirect3D11TextureRenderTarget { + private static readonly Guid IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); + private readonly IPlatformGraphicsContext _context; private readonly WinUiCompositedWindow _window; private readonly IUnknown _d3dDevice; @@ -63,6 +58,7 @@ namespace Avalonia.Win32.WinRT.Composition private PixelSize _size; private bool _lost; private readonly ICompositionDrawingSurfaceInterop _surfaceInterop; + private readonly ICompositionDrawingSurface _drawingSurface; public WinUiCompositedWindowRenderTarget(IPlatformGraphicsContext context, WinUiCompositedWindow window, IntPtr device, @@ -83,28 +79,33 @@ namespace Avalonia.Win32.WinRT.Composition _surface = _drawingSurface.QueryInterface(); _surfaceInterop = _drawingSurface.QueryInterface(); } - finally + catch { - if (_surfaceInterop == null) - Dispose(); + _surface?.Dispose(); + _surfaceInterop?.Dispose(); + _drawingSurface?.Dispose(); + _compositionDevice2?.Dispose(); + _compositionDevice?.Dispose(); + _interop?.Dispose(); + _compositor?.Dispose(); + _d3dDevice?.Dispose(); + throw; } } public void Dispose() { - _surface?.Dispose(); - _surfaceInterop?.Dispose(); - _drawingSurface?.Dispose(); - _compositionDevice2?.Dispose(); - _compositionDevice?.Dispose(); - _interop?.Dispose(); - _compositor?.Dispose(); - _d3dDevice?.Dispose(); + _surface.Dispose(); + _surfaceInterop.Dispose(); + _drawingSurface.Dispose(); + _compositionDevice2.Dispose(); + _compositionDevice.Dispose(); + _interop.Dispose(); + _compositor.Dispose(); + _d3dDevice.Dispose(); } public bool IsCorrupted => _context.IsLost || _lost; - private static Guid IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); - private readonly ICompositionDrawingSurface _drawingSurface; public unsafe IDirect3D11TextureRenderTargetRenderSession BeginDraw() { @@ -155,12 +156,12 @@ namespace Avalonia.Win32.WinRT.Composition { if (needsEndDraw) _surfaceInterop.EndDraw(); - transaction?.Dispose(); + transaction.Dispose(); } } } - class Session : IDirect3D11TextureRenderTargetRenderSession + private class Session : IDirect3D11TextureRenderTargetRenderSession { private readonly IDisposable _transaction; private readonly PixelSize _size; @@ -200,4 +201,4 @@ namespace Avalonia.Win32.WinRT.Composition public double Scaling => _scaling; } } -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs index 301e605e7a..602d0a3f8a 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs @@ -8,11 +8,11 @@ internal class WinUiCompositionShared : IDisposable public ICompositor Compositor { get; } public ICompositor5 Compositor5 { get; } public ICompositorDesktopInterop DesktopInterop { get; } - public ICompositionBrush BlurBrush; - public ICompositionBrush MicaBrush; + public ICompositionBrush BlurBrush { get; } + public ICompositionBrush? MicaBrush { get; } public object SyncRoot { get; } = new(); - public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); + public static readonly Version MinHostBackdropVersion = new(10, 0, 22000); public WinUiCompositionShared(ICompositor compositor) { @@ -26,9 +26,9 @@ internal class WinUiCompositionShared : IDisposable public void Dispose() { BlurBrush.Dispose(); - MicaBrush.Dispose(); + MicaBrush?.Dispose(); DesktopInterop.Dispose(); Compositor.Dispose(); Compositor5.Dispose(); } -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs index e7af4046d6..7b970868df 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs @@ -1,4 +1,3 @@ -using System; using System.Numerics; using MicroCom.Runtime; @@ -6,7 +5,7 @@ namespace Avalonia.Win32.WinRT.Composition; internal static class WinUiCompositionUtils { - public static ICompositionBrush CreateMicaBackdropBrush(ICompositor compositor) + public static ICompositionBrush? CreateMicaBackdropBrush(ICompositor compositor) { if (Win32Platform.WindowsVersion.Build < 22000) return null; @@ -18,7 +17,7 @@ internal static class WinUiCompositionUtils return blurredWallpaperBackdropBrush?.QueryInterface(); } - public static unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush(ICompositor compositor) + public static ICompositionBrush CreateAcrylicBlurBackdropBrush(ICompositor compositor) { using var backDropParameterFactory = NativeWinRTMethods.CreateActivationFactory( @@ -39,7 +38,7 @@ internal static class WinUiCompositionUtils return compositionEffectBrush.QueryInterface(); } - public static ICompositionRoundedRectangleGeometry ClipVisual(ICompositor compositor, float? _backdropCornerRadius, params IVisual[] containerVisuals) + public static ICompositionRoundedRectangleGeometry? ClipVisual(ICompositor compositor, float? _backdropCornerRadius, params IVisual?[] containerVisuals) { if (!_backdropCornerRadius.HasValue) return null; @@ -77,7 +76,7 @@ internal static class WinUiCompositionUtils public static ICompositionBrush CreateBackdropBrush(ICompositor compositor) { - ICompositionBackdropBrush brush = null; + ICompositionBackdropBrush? brush = null; try { if (Win32Platform.WindowsVersion >= WinUiCompositionShared.MinHostBackdropVersion) @@ -98,4 +97,4 @@ internal static class WinUiCompositionUtils brush?.Dispose(); } } -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs index a1408baae0..754af86c06 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs @@ -7,20 +7,17 @@ using Avalonia.Logging; using Avalonia.MicroCom; using Avalonia.OpenGL.Egl; using Avalonia.Rendering; -using Avalonia.Win32.DirectX; using Avalonia.Win32.Interop; -using Avalonia.Win32.OpenGl.Angle; -using MicroCom.Runtime; namespace Avalonia.Win32.WinRT.Composition; internal class WinUiCompositorConnection : IRenderTimer { private readonly WinUiCompositionShared _shared; - public event Action Tick; + public event Action? Tick; public bool RunsInBackground => true; - public unsafe WinUiCompositorConnection() + public WinUiCompositorConnection() { using var compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); /* @@ -42,10 +39,9 @@ internal class WinUiCompositorConnection : IRenderTimer _shared = new WinUiCompositionShared(compositor); } - static bool TryCreateAndRegisterCore() + private static bool TryCreateAndRegisterCore() { var tcs = new TaskCompletionSource(); - var pumpLock = new object(); var th = new Thread(() => { WinUiCompositorConnection connect; @@ -80,10 +76,10 @@ internal class WinUiCompositorConnection : IRenderTimer return tcs.Task.Result; } - class RunLoopHandler : CallbackBase, IAsyncActionCompletedHandler + private class RunLoopHandler : CallbackBase, IAsyncActionCompletedHandler { private readonly WinUiCompositorConnection _parent; - private Stopwatch _st = Stopwatch.StartNew(); + private readonly Stopwatch _st = Stopwatch.StartNew(); public RunLoopHandler(WinUiCompositorConnection parent) { @@ -101,7 +97,7 @@ internal class WinUiCompositorConnection : IRenderTimer private void RunLoop() { var cts = new CancellationTokenSource(); - AppDomain.CurrentDomain.ProcessExit += (sender, args) => + AppDomain.CurrentDomain.ProcessExit += (_, _) => cts.Cancel(); lock (_shared.SyncRoot) diff --git a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs index 01b45f3df2..21f83addeb 100644 --- a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs +++ b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs @@ -1,12 +1,6 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Reflection; using System.Runtime.InteropServices; using System.Threading; -using Avalonia.MicroCom; -using Avalonia.Win32.Interop; using MicroCom.Runtime; namespace Avalonia.Win32.WinRT @@ -108,24 +102,24 @@ namespace Avalonia.Win32.WinRT [DllImport("combase.dll", PreserveSig = false)] private static extern IntPtr RoGetActivationFactory(IntPtr activatableClassId, ref Guid iid); - private static bool _initialized; + private static bool s_initialized; private static void EnsureRoInitialized() { - if (_initialized) + if (s_initialized) return; RoInitialize(Thread.CurrentThread.GetApartmentState() == ApartmentState.STA ? RO_INIT_TYPE.RO_INIT_SINGLETHREADED : RO_INIT_TYPE.RO_INIT_MULTITHREADED); - _initialized = true; + s_initialized = true; } } - class HStringInterop : IDisposable + internal class HStringInterop : IDisposable { private IntPtr _s; - private bool _owns; + private readonly bool _owns; - public HStringInterop(string s) + public HStringInterop(string? s) { _s = s == null ? IntPtr.Zero : NativeWinRTMethods.WindowsCreateString(s); _owns = true; @@ -139,7 +133,7 @@ namespace Avalonia.Win32.WinRT public IntPtr Handle => _s; - public unsafe string Value + public unsafe string? Value { get { diff --git a/src/Windows/Avalonia.Win32/WinRT/WinRTInspectable.cs b/src/Windows/Avalonia.Win32/WinRT/WinRTInspectable.cs index 1fde57fe38..aabf148e75 100644 --- a/src/Windows/Avalonia.Win32/WinRT/WinRTInspectable.cs +++ b/src/Windows/Avalonia.Win32/WinRT/WinRTInspectable.cs @@ -2,12 +2,11 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Avalonia.MicroCom; using MicroCom.Runtime; namespace Avalonia.Win32.WinRT { - class WinRTInspectable : IInspectable, IMicroComShadowContainer + internal class WinRTInspectable : IInspectable, IMicroComShadowContainer { public virtual void Dispose() { @@ -25,9 +24,9 @@ namespace Avalonia.Win32.WinRT *iidCount = (ulong) interfaces.Length; } - public IntPtr RuntimeClassName => NativeWinRTMethods.WindowsCreateString(GetType().FullName); + public IntPtr RuntimeClassName => NativeWinRTMethods.WindowsCreateString(GetType().FullName!); public TrustLevel TrustLevel => TrustLevel.BaseTrust; - public MicroComShadow Shadow { get; set; } + public MicroComShadow? Shadow { get; set; } public virtual void OnReferencedFromNative() { } diff --git a/src/Windows/Avalonia.Win32/WinScreen.cs b/src/Windows/Avalonia.Win32/WinScreen.cs index 1038f41a17..69d334cbcc 100644 --- a/src/Windows/Avalonia.Win32/WinScreen.cs +++ b/src/Windows/Avalonia.Win32/WinScreen.cs @@ -15,14 +15,16 @@ namespace Avalonia.Win32 public IntPtr Handle => _hMonitor; + /// public override int GetHashCode() { - return (int)_hMonitor; + return _hMonitor.GetHashCode(); } - public override bool Equals(object obj) + /// + public override bool Equals(object? obj) { - return (obj is WinScreen screen) ? _hMonitor == screen._hMonitor : base.Equals(obj); + return obj is WinScreen screen && _hMonitor == screen._hMonitor; } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 8c362b0c29..288bdf2448 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -1,12 +1,9 @@ using System; -using System.Buffers; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; -using System.Text; using Avalonia.Automation.Peers; using Avalonia.Controls; -using Avalonia.Controls.Remote; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; @@ -27,9 +24,9 @@ namespace Avalonia.Win32 protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { const double wheelDelta = 120.0; - const long UiaRootObjectId = -25; + const long uiaRootObjectId = -25; uint timestamp = unchecked((uint)GetMessageTime()); - RawInputEventArgs e = null; + RawInputEventArgs? e = null; var shouldTakeFocus = false; var message = (WindowsMessage)msg; switch (message) @@ -92,7 +89,7 @@ namespace Avalonia.Win32 } // We need to release IMM context and state to avoid leaks. - if (Imm32InputMethod.Current.HWND == _hwnd) + if (Imm32InputMethod.Current.Hwnd == _hwnd) { Imm32InputMethod.Current.ClearLanguageAndWindow(); } @@ -104,7 +101,7 @@ namespace Avalonia.Win32 Closed?.Invoke(); _mouseDevice.Dispose(); - _touchDevice?.Dispose(); + _touchDevice.Dispose(); //Free other resources Dispose(); @@ -140,7 +137,7 @@ namespace Avalonia.Win32 { var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); - if (key != Key.None) + if (key != Key.None && _owner is not null) { e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, @@ -170,7 +167,7 @@ namespace Avalonia.Win32 { var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); - if (key != Key.None) + if (key != Key.None && _owner is not null) { e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, @@ -185,7 +182,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_CHAR: { // Ignore control chars and chars that were handled in WM_KEYDOWN. - if (ToInt32(wParam) >= 32 && !_ignoreWmChar) + if (ToInt32(wParam) >= 32 && !_ignoreWmChar && _owner is not null) { e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, new string((char)ToInt32(wParam), 1)); @@ -204,7 +201,7 @@ namespace Avalonia.Win32 break; } shouldTakeFocus = ShouldTakeFocusOnClick; - if (ShouldIgnoreTouchEmulatedMessage()) + if (ShouldIgnoreTouchEmulatedMessage() || _owner is null) { break; } @@ -213,7 +210,9 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, +#pragma warning disable CS8509 message switch +#pragma warning restore CS8509 { WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, @@ -221,7 +220,7 @@ namespace Avalonia.Win32 WindowsMessage.WM_XBUTTONDOWN => HighWord(ToInt32(wParam)) == 1 ? RawPointerEventType.XButton1Down : - RawPointerEventType.XButton2Down + RawPointerEventType.XButton2Down, }, DipFromLParam(lParam), GetMouseModifiers(wParam)); break; @@ -236,7 +235,7 @@ namespace Avalonia.Win32 { break; } - if (ShouldIgnoreTouchEmulatedMessage()) + if (ShouldIgnoreTouchEmulatedMessage() || _owner is null) { break; } @@ -245,7 +244,9 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, +#pragma warning disable CS8509 message switch +#pragma warning restore CS8509 { WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, @@ -309,6 +310,11 @@ namespace Avalonia.Win32 var prevPoint = _lastWmMousePoint; _lastWmMousePoint = currPoint; + if (_owner is null) + { + break; + } + e = new RawPointerEventArgs( _mouseDevice, timestamp, @@ -317,7 +323,7 @@ namespace Avalonia.Win32 point, GetMouseModifiers(wParam)) { - IntermediatePoints = new Lazy>(() => CreateLazyIntermediatePoints(currPoint, prevPoint)) + IntermediatePoints = new Lazy?>(() => CreateIntermediatePoints(currPoint, prevPoint)) }; break; @@ -325,7 +331,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { - if (IsMouseInPointerEnabled) + if (IsMouseInPointerEnabled || _owner is null) { break; } @@ -341,7 +347,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEHWHEEL: { - if (IsMouseInPointerEnabled) + if (IsMouseInPointerEnabled || _owner is null) { break; } @@ -362,6 +368,10 @@ namespace Avalonia.Win32 break; } _trackingMouse = false; + if (_owner is null) + { + break; + } e = new RawPointerEventArgs( _mouseDevice, timestamp, @@ -377,7 +387,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { - if (IsMouseInPointerEnabled) + if (IsMouseInPointerEnabled || _owner is null) { break; } @@ -385,7 +395,9 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, +#pragma warning disable CS8509 message switch +#pragma warning restore CS8509 { WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType .NonClientLeftButtonDown, @@ -396,12 +408,12 @@ namespace Avalonia.Win32 RawPointerEventType.XButton1Down : RawPointerEventType.XButton2Down, }, - PointToClient(WindowImpl.PointFromLParam(lParam)), GetMouseModifiers(wParam)); + PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); break; } case WindowsMessage.WM_TOUCH: { - if (_wmPointerEnabled) + if (_wmPointerEnabled || _owner is null || Input is not { } input) { break; } @@ -414,7 +426,7 @@ namespace Avalonia.Win32 { foreach (var touchInput in touchInputs) { - Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, + input.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, _owner, touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_UP) ? RawPointerEventType.TouchEnd : @@ -438,14 +450,14 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUP: case WindowsMessage.WM_POINTERUPDATE: { - if (!_wmPointerEnabled) + if (!_wmPointerEnabled || _owner is null) { break; } GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); var eventType = GetEventType(message, info); - var args = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); + var args = CreatePointerArgs(device, timestamp, _owner, eventType, point, modifiers, info.pointerId); args.IntermediatePoints = CreateLazyIntermediatePoints(info); e = args; break; @@ -454,19 +466,19 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERLEAVE: case WindowsMessage.WM_POINTERCAPTURECHANGED: { - if (!_wmPointerEnabled) + if (!_wmPointerEnabled || _owner is null) { break; } GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); var eventType = device is TouchDevice ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; - e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); + e = CreatePointerArgs(device, timestamp, _owner, eventType, point, modifiers, info.pointerId); break; } case WindowsMessage.WM_POINTERWHEEL: case WindowsMessage.WM_POINTERHWHEEL: { - if (!_wmPointerEnabled) + if (!_wmPointerEnabled || _owner is null) { break; } @@ -488,7 +500,7 @@ namespace Avalonia.Win32 } // Do not generate events, but release mouse capture on any other device input. - GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); + GetDevicePointerInfo(wParam, out var device, out _, out _, out _, ref timestamp); if (device != _mouseDevice) { _mouseDevice.Capture(null); @@ -726,9 +738,9 @@ namespace Avalonia.Win32 break; case WindowsMessage.WM_GETOBJECT: - if ((long)lParam == UiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable) + if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner is Control control) { - var peer = ControlAutomationPeer.CreatePeerForElement((Control)_owner); + var peer = ControlAutomationPeer.CreatePeerForElement(control); var node = AutomationNode.GetOrCreate(peer); return UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, wParam, lParam, node); } @@ -775,12 +787,12 @@ namespace Avalonia.Win32 } } - private unsafe Lazy> CreateLazyIntermediatePoints(POINTER_INFO info) + private Lazy?>? CreateLazyIntermediatePoints(POINTER_INFO info) { var historyCount = Math.Min((int)info.historyCount, MaxPointerHistorySize); if (historyCount > 1) { - return new Lazy>(() => + return new Lazy?>(() => { s_intermediatePointsPooledList.Clear(); s_intermediatePointsPooledList.Capacity = historyCount; @@ -829,7 +841,7 @@ namespace Avalonia.Win32 return null; } - private unsafe IReadOnlyList CreateLazyIntermediatePoints(MOUSEMOVEPOINT movePoint, MOUSEMOVEPOINT prevMovePoint) + private unsafe IReadOnlyList CreateIntermediatePoints(MOUSEMOVEPOINT movePoint, MOUSEMOVEPOINT prevMovePoint) { // To understand some of this code, please check MS docs: // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmousemovepointsex#remarks @@ -888,11 +900,11 @@ namespace Avalonia.Win32 } } - private RawPointerEventArgs CreatePointerArgs(IInputDevice device, ulong timestamp, RawPointerEventType eventType, RawPointerPoint point, RawInputModifiers modifiers, uint rawPointerId) + private RawPointerEventArgs CreatePointerArgs(IInputDevice device, ulong timestamp, IInputRoot owner, RawPointerEventType eventType, RawPointerPoint point, RawInputModifiers modifiers, uint rawPointerId) { return device is TouchDevice - ? new RawTouchEventArgs(device, timestamp, _owner, eventType, point, modifiers, rawPointerId) - : new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers) + ? new RawTouchEventArgs(device, timestamp, owner, eventType, point, modifiers, rawPointerId) + : new RawPointerEventArgs(device, timestamp, owner, eventType, point, modifiers) { RawPointerId = rawPointerId }; @@ -1029,7 +1041,7 @@ namespace Avalonia.Win32 // note: for non-ime language, also create it so that emoji panel tracks cursor var langid = LGID(hkl); - if (langid == _langid && Imm32InputMethod.Current.HWND == Hwnd) + if (langid == _langid && Imm32InputMethod.Current.Hwnd == Hwnd) { return; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index fb27ab7856..20ab738a2a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -3,8 +3,6 @@ using Avalonia.Controls; using Avalonia.Input; using static Avalonia.Win32.Interop.UnmanagedMethods; -#nullable enable - namespace Avalonia.Win32 { public partial class WindowImpl @@ -13,7 +11,7 @@ namespace Avalonia.Win32 private HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam) { // Get the point coordinates for the hit test (screen space). - var ptMouse = WindowImpl.PointFromLParam(lParam); + var ptMouse = PointFromLParam(lParam); // Get the window rectangle. GetWindowRect(hWnd, out var rcWindow); @@ -101,11 +99,9 @@ namespace Avalonia.Win32 lRet = (IntPtr)hittestResult; - uint timestamp = unchecked((uint)GetMessageTime()); - if (hittestResult == HitTestValues.HTCAPTION) { - var position = PointToClient(WindowImpl.PointFromLParam(lParam)); + var position = PointToClient(PointFromLParam(lParam)); if (_owner is Window window) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 07f2311be8..49197a83e8 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -2,14 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Win32.Input; -using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 1e0d92d442..57407b5f34 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1,33 +1,29 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; -using Avalonia.Controls; -using Avalonia.Automation.Peers; +using Avalonia.Collections.Pooled; using Avalonia.Controls.Platform; -using Avalonia.Input; +using Avalonia.Controls; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Angle; +using Avalonia.Input; +using Avalonia.Metadata; using Avalonia.OpenGL.Egl; -using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform.Storage; using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using Avalonia.Win32.Automation; +using Avalonia.Rendering; +using Avalonia.Win32.DirectX; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; +using Avalonia.Win32.OpenGl.Angle; using Avalonia.Win32.OpenGl; -using Avalonia.Win32.WinRT; using Avalonia.Win32.WinRT.Composition; +using Avalonia.Win32.WinRT; using static Avalonia.Win32.Interop.UnmanagedMethods; -using Avalonia.Collections.Pooled; -using Avalonia.Metadata; -using Avalonia.Platform.Storage; -using Avalonia.Win32.DirectX; -using Avalonia.Win32.OpenGl.Angle; namespace Avalonia.Win32 { @@ -37,13 +33,13 @@ namespace Avalonia.Win32 [Unstable] public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo { - private static readonly List s_instances = new List(); + private static readonly List s_instances = new(); - private static readonly IntPtr DefaultCursor = LoadCursor( + private static readonly IntPtr s_defaultCursor = LoadCursor( IntPtr.Zero, new IntPtr((int)UnmanagedMethods.Cursor.IDC_ARROW)); private static readonly Dictionary s_edgeLookup = - new Dictionary + new() { { WindowEdge.East, HitTestValues.HTRIGHT }, { WindowEdge.North, HitTestValues.HTTOP }, @@ -61,8 +57,8 @@ namespace Avalonia.Win32 private Thickness _extendedMargins; private Thickness _offScreenMargin; private double _extendTitleBarHint = -1; - private bool _isUsingComposition; - private IBlurHost _blurHost; + private readonly bool _isUsingComposition; + private readonly IBlurHost? _blurHost; private PlatformResizeReason _resizeReason; private MOUSEMOVEPOINT _lastWmMousePoint; @@ -76,26 +72,26 @@ namespace Avalonia.Win32 private readonly PenDevice _penDevice; private readonly ManagedDeferredRendererLock _rendererLock; private readonly FramebufferManager _framebuffer; - private readonly object _gl; + private readonly object? _gl; private readonly bool _wmPointerEnabled; - private Win32NativeControlHost _nativeControlHost; - private IStorageProvider _storageProvider; + private readonly Win32NativeControlHost _nativeControlHost; + private readonly IStorageProvider _storageProvider; private WndProc _wndProcDelegate; - private string _className; + private string? _className; private IntPtr _hwnd; - private IInputRoot _owner; + private IInputRoot? _owner; private WindowProperties _windowProperties; private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk private bool _topmost; private double _scaling = 1; private WindowState _showWindowState; private WindowState _lastWindowState; - private OleDropTarget _dropTarget; + private OleDropTarget? _dropTarget; private Size _minSize; private Size _maxSize; private POINT _maxTrackSize; - private WindowImpl _parent; + private WindowImpl? _parent; private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; private bool _isCloseRequested; private bool _shown; @@ -142,7 +138,7 @@ namespace Avalonia.Win32 angle.PlatformApi == AngleOptions.PlatformApi.DirectX11; _isUsingComposition = compositionConnector is { } && isUsingAngleDX11; - DxgiConnection dxgiConnection = null; + DxgiConnection? dxgiConnection = null; var isUsingDxgiSwapchain = false; if (!_isUsingComposition) { @@ -159,23 +155,20 @@ namespace Avalonia.Win32 { if (_isUsingComposition) { - var cgl = compositionConnector.CreateSurface(this); + var cgl = compositionConnector!.CreateSurface(this); _blurHost = cgl; - _gl = cgl; - - _isUsingComposition = true; } else if (isUsingDxgiSwapchain) { - var dxgigl = new DxgiSwapchainWindow(dxgiConnection, this); + var dxgigl = new DxgiSwapchainWindow(dxgiConnection!, this); _gl = dxgigl; } else { - if (glPlatform is AngleWin32PlatformGraphics egl2) + if (glPlatform is AngleWin32PlatformGraphics) _gl = new EglGlPlatformSurface(this); - else if (glPlatform is WglPlatformOpenGlInterface wgl) + else if (glPlatform is WglPlatformOpenGlInterface) _gl = new WglGlPlatformSurface(this); } } @@ -187,29 +180,41 @@ namespace Avalonia.Win32 s_instances.Add(this); } - public Action Activated { get; set; } + /// + public Action? Activated { get; set; } - public Func Closing { get; set; } + /// + public Func? Closing { get; set; } - public Action Closed { get; set; } + /// + public Action? Closed { get; set; } - public Action Deactivated { get; set; } + /// + public Action? Deactivated { get; set; } - public Action Input { get; set; } + /// + public Action? Input { get; set; } - public Action Paint { get; set; } + /// + public Action? Paint { get; set; } - public Action Resized { get; set; } + /// + public Action? Resized { get; set; } - public Action ScalingChanged { get; set; } + /// + public Action? ScalingChanged { get; set; } - public Action PositionChanged { get; set; } + /// + public Action? PositionChanged { get; set; } - public Action WindowStateChanged { get; set; } + /// + public Action? WindowStateChanged { get; set; } - public Action LostFocus { get; set; } + /// + public Action? LostFocus { get; set; } - public Action TransparencyLevelChanged { get; set; } + /// + public Action? TransparencyLevelChanged { get; set; } public Thickness BorderThickness { @@ -240,10 +245,13 @@ namespace Avalonia.Win32 private double PrimaryScreenRenderScaling => Screen.AllScreens.FirstOrDefault(screen => screen.IsPrimary)?.Scaling ?? 1; + /// public double RenderScaling => _scaling; + /// public double DesktopScaling => RenderScaling; + /// public Size ClientSize { get @@ -254,6 +262,7 @@ namespace Avalonia.Win32 } } + /// public Size? FrameSize { get @@ -269,14 +278,18 @@ namespace Avalonia.Win32 } } + /// public IScreenImpl Screen { get; } + /// public IPlatformHandle Handle { get; private set; } + /// public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / RenderScaling, _maxTrackSize.Y / RenderScaling); public IMouseDevice MouseDevice => _mouseDevice; + /// public WindowState WindowState { get @@ -314,13 +327,15 @@ namespace Avalonia.Win32 } } + /// public WindowTransparencyLevel TransparencyLevel { get; private set; } protected IntPtr Hwnd => _hwnd; private bool IsMouseInPointerEnabled => _wmPointerEnabled && IsMouseInPointerEnabled(); - public object TryGetFeature(Type featureType) + /// + public object? TryGetFeature(Type featureType) { if (featureType == typeof(ITextInputMethodImpl)) { @@ -339,7 +354,8 @@ namespace Avalonia.Win32 return null; } - + + /// public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); @@ -521,8 +537,13 @@ namespace Avalonia.Win32 } } - public IEnumerable Surfaces => new object[] { (IPlatformNativeSurfaceHandle)Handle, _gl, _framebuffer }; + /// + public IEnumerable Surfaces + => _gl is null ? + new object[] { Handle, _framebuffer } : + new object[] { Handle, _gl, _framebuffer }; + /// public PixelPoint Position { get @@ -568,17 +589,21 @@ namespace Avalonia.Win32 } } + /// public void Move(PixelPoint point) => Position = point; + /// public void SetMinMaxSize(Size minSize, Size maxSize) { _minSize = minSize; _maxSize = maxSize; } + /// public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Win32Platform.Compositor, () => Surfaces); + /// public void Resize(Size value, PlatformResizeReason reason) { if (WindowState != WindowState.Normal) @@ -606,13 +631,16 @@ namespace Avalonia.Win32 } } + /// public void Activate() { SetForegroundWindow(_hwnd); } - public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); + /// + public IPopupImpl? CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); + /// public void Dispose() { (_gl as IDisposable)?.Dispose(); @@ -654,6 +682,7 @@ namespace Avalonia.Win32 InvalidateRect(_hwnd, ref r, false); } + /// public Point PointToClient(PixelPoint point) { var p = new POINT { X = point.X, Y = point.Y }; @@ -661,6 +690,7 @@ namespace Avalonia.Win32 return new Point(p.X, p.Y) / RenderScaling; } + /// public PixelPoint PointToScreen(Point point) { point *= RenderScaling; @@ -669,29 +699,34 @@ namespace Avalonia.Win32 return new PixelPoint(p.X, p.Y); } + /// public void SetInputRoot(IInputRoot inputRoot) { _owner = inputRoot; - CreateDropTarget(); + CreateDropTarget(inputRoot); } + /// public void Hide() { UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); _shown = false; } + /// public virtual void Show(bool activate, bool isDialog) { SetParent(_parent); ShowWindow(_showWindowState, activate); } - public Action GotInputWhenDisabled { get; set; } + /// + public Action? GotInputWhenDisabled { get; set; } - public void SetParent(IWindowImpl parent) + /// + public void SetParent(IWindowImpl? parent) { - _parent = (WindowImpl)parent; + _parent = parent as WindowImpl; var parentHwnd = _parent?._hwnd ?? IntPtr.Zero; @@ -704,8 +739,10 @@ namespace Avalonia.Win32 SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd); } + /// public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); + /// public void BeginMoveDrag(PointerPressedEventArgs e) { e.Pointer.Capture(null); @@ -713,6 +750,7 @@ namespace Avalonia.Win32 new IntPtr((int)HitTestValues.HTCAPTION), IntPtr.Zero); } + /// public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { if (_windowProperties.IsResizable) @@ -727,35 +765,37 @@ namespace Avalonia.Win32 } } - public void SetTitle(string title) + /// + public void SetTitle(string? title) { SetWindowText(_hwnd, title); } - public void SetCursor(ICursorImpl cursor) + /// + public void SetCursor(ICursorImpl? cursor) { var impl = cursor as CursorImpl; - if (cursor is null || impl is object) - { - var hCursor = impl?.Handle ?? DefaultCursor; - SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); + var hCursor = impl?.Handle ?? s_defaultCursor; + SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); - if (_owner.IsPointerOver) - { - UnmanagedMethods.SetCursor(hCursor); - } + if (_owner?.IsPointerOver == true) + { + UnmanagedMethods.SetCursor(hCursor); } } - public void SetIcon(IWindowIconImpl icon) + /// + public void SetIcon(IWindowIconImpl? icon) { - var impl = (IconImpl)icon; + var impl = icon as IconImpl; + var hIcon = impl?.HIcon ?? IntPtr.Zero; PostMessage(_hwnd, (int)WindowsMessage.WM_SETICON, new IntPtr((int)Icons.ICON_BIG), hIcon); } + /// public void ShowTaskbarIcon(bool value) { var newWindowProperties = _windowProperties; @@ -765,6 +805,7 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); } + /// public void CanResize(bool value) { var newWindowProperties = _windowProperties; @@ -774,6 +815,7 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); } + /// public void SetSystemDecorations(SystemDecorations value) { var newWindowProperties = _windowProperties; @@ -783,6 +825,7 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); } + /// public void SetTopmost(bool value) { if (value == _topmost) @@ -799,6 +842,7 @@ namespace Avalonia.Win32 _topmost = value; } + /// public unsafe void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { if (Win32Platform.WindowsVersion.Build >= 22000) @@ -829,6 +873,9 @@ namespace Avalonia.Win32 IntPtr.Zero); } + [MemberNotNull(nameof(_wndProcDelegate))] + [MemberNotNull(nameof(_className))] + [MemberNotNull(nameof(Handle))] private void CreateWindow() { // Ensure that the delegate doesn't get garbage collected by storing it as a field. @@ -845,7 +892,7 @@ namespace Avalonia.Win32 style = (int)windowClassStyle, lpfnWndProc = _wndProcDelegate, hInstance = GetModuleHandle(null), - hCursor = DefaultCursor, + hCursor = s_defaultCursor, hbrBackground = IntPtr.Zero, lpszClassName = _className }; @@ -878,20 +925,23 @@ namespace Avalonia.Win32 monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpix, - out var dpiy) == 0) + out _) == 0) { _scaling = dpix / 96.0; } } } - private void CreateDropTarget() + private void CreateDropTarget(IInputRoot inputRoot) { - var odt = new OleDropTarget(this, _owner); - - if (OleContext.Current?.RegisterDragDrop(Handle, odt) ?? false) + if (AvaloniaLocator.Current.GetService() is { } dragDropDevice) { - _dropTarget = odt; + var odt = new OleDropTarget(this, inputRoot, dragDropDevice); + + if (OleContext.Current?.RegisterDragDrop(Handle, odt) ?? false) + { + _dropTarget = odt; + } } } @@ -1105,7 +1155,7 @@ namespace Avalonia.Win32 case WindowState.FullScreen: newWindowProperties.IsFullScreen = true; - command = IsWindowVisible(_hwnd) ? (ShowWindowCommand?)null : ShowWindowCommand.Restore; + command = IsWindowVisible(_hwnd) ? null : ShowWindowCommand.Restore; break; default: @@ -1370,13 +1420,13 @@ namespace Avalonia.Win32 private const int MF_DISABLED = 0x2; private const int SC_CLOSE = 0xF060; - static void DisableCloseButton(IntPtr hwnd) + private static void DisableCloseButton(IntPtr hwnd) { EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); } - static void EnableCloseButton(IntPtr hwnd) + private static void EnableCloseButton(IntPtr hwnd) { EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, MF_BYCOMMAND | MF_ENABLED); @@ -1407,6 +1457,7 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + /// public void SetExtendClientAreaToDecorationsHint(bool hint) { _isClientAreaExtended = hint; @@ -1414,6 +1465,7 @@ namespace Avalonia.Win32 ExtendClientArea(); } + /// public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { _extendChromeHints = hints; @@ -1433,7 +1485,7 @@ namespace Avalonia.Win32 public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended; /// - public Action ExtendClientAreaToDecorationsChanged { get; set; } + public Action? ExtendClientAreaToDecorationsChanged { get; set; } /// public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome); diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index 3244e75a34..f2da804107 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -2,7 +2,6 @@ using System; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using Avalonia.Reactive; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Threading; @@ -12,12 +11,12 @@ namespace Avalonia.Win32 internal class WindowsMountedVolumeInfoListener : IDisposable { private readonly IDisposable _disposable; - private bool _beenDisposed = false; - private ObservableCollection mountedDrives; + private bool _beenDisposed; + private readonly ObservableCollection _mountedDrives; public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) { - this.mountedDrives = mountedDrives; + _mountedDrives = mountedDrives; _disposable = DispatcherTimer.Run(Poll, TimeSpan.FromSeconds(1)); @@ -51,14 +50,14 @@ namespace Avalonia.Win32 }) .ToArray(); - if (mountedDrives.SequenceEqual(mountVolInfos)) + if (_mountedDrives.SequenceEqual(mountVolInfos)) return true; else { - mountedDrives.Clear(); + _mountedDrives.Clear(); foreach (var i in mountVolInfos) - mountedDrives.Add(i); + _mountedDrives.Add(i); return true; } } diff --git a/src/tools/DevGenerators/Helpers.cs b/src/tools/DevGenerators/Helpers.cs index 3da89d2d0e..9cebf6daea 100644 --- a/src/tools/DevGenerators/Helpers.cs +++ b/src/tools/DevGenerators/Helpers.cs @@ -6,16 +6,21 @@ namespace Generator; static class Helpers { + private static readonly SymbolDisplayFormat s_symbolDisplayFormat = + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + public static StringBuilder Pad(this StringBuilder sb, int count) => sb.Append(' ', count * 4); public static string GetFullyQualifiedName(this ISymbol symbol) { - return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return symbol.ToDisplayString(s_symbolDisplayFormat); } public static bool HasFullyQualifiedName(this ISymbol symbol, string name) { - return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name; + return symbol.ToDisplayString(s_symbolDisplayFormat) == name; } public static bool HasAttributeWithFullyQualifiedName(this ISymbol symbol, string name) @@ -28,4 +33,4 @@ static class Helpers return false; } -} \ No newline at end of file +} From 2273534a57821b5fe39b374e7610e518e624a0be Mon Sep 17 00:00:00 2001 From: Stan Wijckmans Date: Mon, 5 Sep 2022 22:15:39 +0200 Subject: [PATCH 038/108] Add API to prevent App Nap. --- .../project.pbxproj | 4 ++ .../src/OSX/PlatformBehaviorInhibition.mm | 39 +++++++++++++++++++ native/Avalonia.Native/src/OSX/common.h | 1 + native/Avalonia.Native/src/OSX/main.mm | 11 ++++++ .../Platform/IPlatformBehaviorInhibition.cs | 12 ++++++ .../PlatformInhibitionType.cs | 13 +++++++ src/Avalonia.Controls/TopLevel.cs | 25 ++++++++++++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 21 ---------- .../PlatformBehaviorInhibition.cs | 20 ++++++++++ src/Avalonia.Native/avn.idl | 7 ++++ 10 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/PlatformBehaviorInhibition.mm create mode 100644 src/Avalonia.Base/Platform/IPlatformBehaviorInhibition.cs create mode 100644 src/Avalonia.Controls/PlatformInhibitionType.cs create mode 100644 src/Avalonia.Native/PlatformBehaviorInhibition.cs diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 41d1534f8d..c49290314d 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; + 855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */ = {isa = PBXBuildFile; fileRef = 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; }; AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; @@ -95,6 +96,7 @@ 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; + 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformBehaviorInhibition.mm; sourceTree = ""; }; AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -140,6 +142,7 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */, BC11A5BC2608D58F0017BAD0 /* automation.h */, BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, @@ -288,6 +291,7 @@ 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, + 855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/PlatformBehaviorInhibition.mm b/native/Avalonia.Native/src/OSX/PlatformBehaviorInhibition.mm new file mode 100644 index 0000000000..db054d82ef --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PlatformBehaviorInhibition.mm @@ -0,0 +1,39 @@ +#include "common.h" + +namespace +{ + id s_inhibitAppSleepHandle{}; +} + +class PlatformBehaviorInhibition : public ComSingleObject +{ +public: + FORWARD_IUNKNOWN() + + virtual void SetInhibitAppSleep(bool inhibitAppSleep, char* reason) override + { + START_COM_CALL; + + @autoreleasepool + { + if (inhibitAppSleep && s_inhibitAppSleepHandle == nullptr) + { + NSActivityOptions options = NSActivityUserInitiatedAllowingIdleSystemSleep; + s_inhibitAppSleepHandle = [[NSProcessInfo processInfo] beginActivityWithOptions:options reason:[NSString stringWithUTF8String: reason]]; + } + + if (!inhibitAppSleep) + { + s_inhibitAppSleepHandle = nullptr; + } + } + } +}; + +extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition() +{ + @autoreleasepool + { + return new PlatformBehaviorInhibition(); + } +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 972927b99d..4353737dc8 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -26,6 +26,7 @@ extern IAvnTrayIcon* CreateTrayIcon(); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnApplicationCommands* CreateApplicationCommands(); +extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern IAvnPlatformSettings* CreatePlatformSettings(); extern void SetAppMenu(IAvnMenu *menu); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 99063e600e..4bfda4b531 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -408,6 +408,17 @@ public: return S_OK; } } + + virtual HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreatePlatformBehaviorInhibition(); + return S_OK; + } + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/src/Avalonia.Base/Platform/IPlatformBehaviorInhibition.cs b/src/Avalonia.Base/Platform/IPlatformBehaviorInhibition.cs new file mode 100644 index 0000000000..227e65c08d --- /dev/null +++ b/src/Avalonia.Base/Platform/IPlatformBehaviorInhibition.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Avalonia.Platform +{ + /// + /// Allows to inhibit platform specific behavior. + /// + public interface IPlatformBehaviorInhibition + { + Task SetInhibitAppSleep(bool inhibitAppSleep, string reason); + } +} diff --git a/src/Avalonia.Controls/PlatformInhibitionType.cs b/src/Avalonia.Controls/PlatformInhibitionType.cs new file mode 100644 index 0000000000..03e3270e0b --- /dev/null +++ b/src/Avalonia.Controls/PlatformInhibitionType.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Controls +{ + /// + /// A platform specific behavior that can be inhibited. + /// + public enum PlatformInhibitionType + { + /// + /// When inhibited, prevents the app from being put to sleep or being given a lower priority when not in focus. + /// + AppSleep + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index f956fb8724..ed11dec1d0 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -20,6 +20,7 @@ using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.Input.Platform; using System.Linq; +using System.Threading.Tasks; namespace Avalonia.Controls { @@ -570,6 +571,30 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); + /// + /// Requests a to be inhibited. + /// The behavior remains inhibited until the return value is disposed. + /// The available set of s depends on the platform. + /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. + /// + protected async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) + { + var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); + if (platformBehaviorInhibition == null) + { + return Disposable.Create(() => { }); + } + + switch (type) + { + case PlatformInhibitionType.AppSleep: + await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); + return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); + default: + return Disposable.Create(() => { }); + } + } + /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 09feb0c768..67b55bd512 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -158,25 +158,4 @@ namespace Avalonia.Native throw new NotImplementedException(); } } - - public class AvaloniaNativeMacOptions - { - private readonly IAvnMacOptions _opts; - private bool _showInDock; - internal AvaloniaNativeMacOptions(IAvnMacOptions opts) - { - _opts = opts; - ShowInDock = true; - } - - public bool ShowInDock - { - get => _showInDock; - set - { - _showInDock = value; - _opts.SetShowInDock(value ? 1 : 0); - } - } - } } diff --git a/src/Avalonia.Native/PlatformBehaviorInhibition.cs b/src/Avalonia.Native/PlatformBehaviorInhibition.cs new file mode 100644 index 0000000000..dfbfe8a5e5 --- /dev/null +++ b/src/Avalonia.Native/PlatformBehaviorInhibition.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Avalonia.Native.Interop; +using Avalonia.Platform; + +namespace Avalonia.Native +{ + internal class PlatformBehaviorInhibition : IPlatformBehaviorInhibition + { + readonly IAvnPlatformBehaviorInhibition _native; + + internal PlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition native) + => _native = native; + + public Task SetInhibitAppSleep(bool inhibitAppSleep, string reason) + { + _native.SetInhibitAppSleep(inhibitAppSleep ? 1 : 0, reason); + return Task.CompletedTask; + } + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index a062fdc61d..db78873672 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -503,6 +503,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateTrayIcon(IAvnTrayIcon** ppv); HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv); HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv); + HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -924,3 +925,9 @@ interface IAvnPlatformSettings : IUnknown uint GetAccentColor(); void RegisterColorsChange(IAvnActionCallback* callback); } + +[uuid(12edf00d-5803-4d3f-9947-b4840e5e9372)] +interface IAvnPlatformBehaviorInhibition : IUnknown +{ + void SetInhibitAppSleep(bool inhibitAppSleep, char* reason); +} From f4e35493c9176edc146fcd2b73033a6e0363b7b9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 12 Feb 2023 10:52:51 -0500 Subject: [PATCH 039/108] Add missed IPlatformBehaviorInhibition to the TryGetFeature --- src/Avalonia.Native/WindowImplBase.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 50bee0d395..4357f3b705 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -63,6 +63,7 @@ namespace Avalonia.Native private GlPlatformSurface _glSurface; private NativeControlHostImpl _nativeControlHost; private IStorageProvider _storageProvider; + private PlatformBehaviorInhibition _platformBehaviorInhibition; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativeGlPlatformGraphics glFeature) @@ -88,6 +89,7 @@ namespace Avalonia.Native _savedScaling = RenderScaling; _nativeControlHost = new NativeControlHostImpl(_native.CreateNativeControlHost()); _storageProvider = new SystemDialogs(this, _factory.CreateSystemDialogs()); + _platformBehaviorInhibition = new PlatformBehaviorInhibition(_factory.CreatePlatformBehaviorInhibition()); var monitor = Screen.AllScreens.OrderBy(x => x.Scaling) .FirstOrDefault(m => m.Bounds.Contains(Position)); @@ -521,6 +523,11 @@ namespace Avalonia.Native return _storageProvider; } + if (featureType == typeof(IPlatformBehaviorInhibition)) + { + return _platformBehaviorInhibition; + } + return null; } From 2b08668c644225edb78f841c7ac529704dab4623 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 12 Feb 2023 12:14:05 +0100 Subject: [PATCH 040/108] Addressed feedback on PR #10307 --- .../Avalonia.Win32/NonPumpingSyncContext.cs | 43 ++++++++++-- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 67 ++++++++----------- src/Windows/Avalonia.Win32/WindowImpl.cs | 57 ++-------------- 3 files changed, 71 insertions(+), 96 deletions(-) diff --git a/src/Windows/Avalonia.Win32/NonPumpingSyncContext.cs b/src/Windows/Avalonia.Win32/NonPumpingSyncContext.cs index 0c02f643b2..2c4d2c9468 100644 --- a/src/Windows/Avalonia.Win32/NonPumpingSyncContext.cs +++ b/src/Windows/Avalonia.Win32/NonPumpingSyncContext.cs @@ -8,17 +8,42 @@ namespace Avalonia.Win32 { internal class NonPumpingSyncContext : SynchronizationContext, IDisposable { - private readonly SynchronizationContext _inner; + private readonly SynchronizationContext? _inner; - private NonPumpingSyncContext(SynchronizationContext inner) + private NonPumpingSyncContext(SynchronizationContext? inner) { _inner = inner; SetWaitNotificationRequired(); SetSynchronizationContext(this); } - public override void Post(SendOrPostCallback d, object? state) => _inner.Post(d, state); - public override void Send(SendOrPostCallback d, object? state) => _inner.Send(d, state); + public override void Post(SendOrPostCallback d, object? state) + { + if (_inner is null) + { +#if NET6_0_OR_GREATER + ThreadPool.QueueUserWorkItem(static x => x.d(x.state), (d, state), false); +#else + ThreadPool.QueueUserWorkItem(_ => d(state)); +#endif + } + else + { + _inner.Post(d, state); + } + } + + public override void Send(SendOrPostCallback d, object? state) + { + if (_inner is null) + { + d(state); + } + else + { + _inner.Send(d, state); + } + } #if !NET6_0_OR_GREATER [PrePrepareMethod] @@ -34,7 +59,15 @@ namespace Avalonia.Win32 public static IDisposable? Use() { var current = Current; - return current is null or NonPumpingSyncContext ? null : new NonPumpingSyncContext(current); + if (current == null) + { + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + return null; + } + if (current is NonPumpingSyncContext) + return null; + + return new NonPumpingSyncContext(current); } internal class HelperImpl : NonPumpingLockHelper.IHelperImpl diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 288bdf2448..ed80f88b53 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -137,12 +137,12 @@ namespace Avalonia.Win32 { var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); - if (key != Key.None && _owner is not null) + if (key != Key.None) { e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, - _owner, + Owner, RawKeyEventType.KeyDown, key, WindowsKeyboardDevice.Instance.Modifiers); @@ -167,12 +167,12 @@ namespace Avalonia.Win32 { var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); - if (key != Key.None && _owner is not null) + if (key != Key.None) { e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, - _owner, + Owner, RawKeyEventType.KeyUp, key, WindowsKeyboardDevice.Instance.Modifiers); @@ -182,9 +182,9 @@ namespace Avalonia.Win32 case WindowsMessage.WM_CHAR: { // Ignore control chars and chars that were handled in WM_KEYDOWN. - if (ToInt32(wParam) >= 32 && !_ignoreWmChar && _owner is not null) + if (ToInt32(wParam) >= 32 && !_ignoreWmChar) { - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, new string((char)ToInt32(wParam), 1)); } @@ -201,7 +201,7 @@ namespace Avalonia.Win32 break; } shouldTakeFocus = ShouldTakeFocusOnClick; - if (ShouldIgnoreTouchEmulatedMessage() || _owner is null) + if (ShouldIgnoreTouchEmulatedMessage()) { break; } @@ -209,7 +209,7 @@ namespace Avalonia.Win32 e = new RawPointerEventArgs( _mouseDevice, timestamp, - _owner, + Owner, #pragma warning disable CS8509 message switch #pragma warning restore CS8509 @@ -235,7 +235,7 @@ namespace Avalonia.Win32 { break; } - if (ShouldIgnoreTouchEmulatedMessage() || _owner is null) + if (ShouldIgnoreTouchEmulatedMessage()) { break; } @@ -243,7 +243,7 @@ namespace Avalonia.Win32 e = new RawPointerEventArgs( _mouseDevice, timestamp, - _owner, + Owner, #pragma warning disable CS8509 message switch #pragma warning restore CS8509 @@ -310,15 +310,10 @@ namespace Avalonia.Win32 var prevPoint = _lastWmMousePoint; _lastWmMousePoint = currPoint; - if (_owner is null) - { - break; - } - e = new RawPointerEventArgs( _mouseDevice, timestamp, - _owner, + Owner, RawPointerEventType.Move, point, GetMouseModifiers(wParam)) @@ -331,14 +326,14 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { - if (IsMouseInPointerEnabled || _owner is null) + if (IsMouseInPointerEnabled) { break; } e = new RawMouseWheelEventArgs( _mouseDevice, timestamp, - _owner, + Owner, PointToClient(PointFromLParam(lParam)), new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); @@ -347,14 +342,14 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEHWHEEL: { - if (IsMouseInPointerEnabled || _owner is null) + if (IsMouseInPointerEnabled) { break; } e = new RawMouseWheelEventArgs( _mouseDevice, timestamp, - _owner, + Owner, PointToClient(PointFromLParam(lParam)), new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); @@ -368,14 +363,10 @@ namespace Avalonia.Win32 break; } _trackingMouse = false; - if (_owner is null) - { - break; - } e = new RawPointerEventArgs( _mouseDevice, timestamp, - _owner, + Owner, RawPointerEventType.LeaveWindow, new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); @@ -387,14 +378,14 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { - if (IsMouseInPointerEnabled || _owner is null) + if (IsMouseInPointerEnabled) { break; } e = new RawPointerEventArgs( _mouseDevice, timestamp, - _owner, + Owner, #pragma warning disable CS8509 message switch #pragma warning restore CS8509 @@ -413,7 +404,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_TOUCH: { - if (_wmPointerEnabled || _owner is null || Input is not { } input) + if (_wmPointerEnabled || Input is not { } input) { break; } @@ -427,7 +418,7 @@ namespace Avalonia.Win32 foreach (var touchInput in touchInputs) { input.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, - _owner, + Owner, touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_UP) ? RawPointerEventType.TouchEnd : touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_DOWN) ? @@ -450,14 +441,14 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUP: case WindowsMessage.WM_POINTERUPDATE: { - if (!_wmPointerEnabled || _owner is null) + if (!_wmPointerEnabled) { break; } GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); var eventType = GetEventType(message, info); - var args = CreatePointerArgs(device, timestamp, _owner, eventType, point, modifiers, info.pointerId); + var args = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); args.IntermediatePoints = CreateLazyIntermediatePoints(info); e = args; break; @@ -466,19 +457,19 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERLEAVE: case WindowsMessage.WM_POINTERCAPTURECHANGED: { - if (!_wmPointerEnabled || _owner is null) + if (!_wmPointerEnabled) { break; } GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); var eventType = device is TouchDevice ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; - e = CreatePointerArgs(device, timestamp, _owner, eventType, point, modifiers, info.pointerId); + e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); break; } case WindowsMessage.WM_POINTERWHEEL: case WindowsMessage.WM_POINTERHWHEEL: { - if (!_wmPointerEnabled || _owner is null) + if (!_wmPointerEnabled) { break; } @@ -486,7 +477,7 @@ namespace Avalonia.Win32 var val = (ToInt32(wParam) >> 16) / wheelDelta; var delta = message == WindowsMessage.WM_POINTERWHEEL ? new Vector(0, val) : new Vector(val, 0); - e = new RawMouseWheelEventArgs(device, timestamp, _owner, point.Position, delta, modifiers) + e = new RawMouseWheelEventArgs(device, timestamp, Owner, point.Position, delta, modifiers) { RawPointerId = info.pointerId }; @@ -900,11 +891,11 @@ namespace Avalonia.Win32 } } - private RawPointerEventArgs CreatePointerArgs(IInputDevice device, ulong timestamp, IInputRoot owner, RawPointerEventType eventType, RawPointerPoint point, RawInputModifiers modifiers, uint rawPointerId) + private RawPointerEventArgs CreatePointerArgs(IInputDevice device, ulong timestamp, RawPointerEventType eventType, RawPointerPoint point, RawInputModifiers modifiers, uint rawPointerId) { return device is TouchDevice - ? new RawTouchEventArgs(device, timestamp, owner, eventType, point, modifiers, rawPointerId) - : new RawPointerEventArgs(device, timestamp, owner, eventType, point, modifiers) + ? new RawTouchEventArgs(device, timestamp, Owner, eventType, point, modifiers, rawPointerId) + : new RawPointerEventArgs(device, timestamp, Owner, eventType, point, modifiers) { RawPointerId = rawPointerId }; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 57407b5f34..812c72fd3c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -180,40 +180,31 @@ namespace Avalonia.Win32 s_instances.Add(this); } - /// + private IInputRoot Owner + => _owner ?? throw new InvalidOperationException($"{nameof(SetInputRoot)} must have been called"); + public Action? Activated { get; set; } - /// public Func? Closing { get; set; } - /// public Action? Closed { get; set; } - /// public Action? Deactivated { get; set; } - /// public Action? Input { get; set; } - /// public Action? Paint { get; set; } - /// public Action? Resized { get; set; } - /// public Action? ScalingChanged { get; set; } - /// public Action? PositionChanged { get; set; } - /// public Action? WindowStateChanged { get; set; } - /// public Action? LostFocus { get; set; } - /// public Action? TransparencyLevelChanged { get; set; } public Thickness BorderThickness @@ -245,13 +236,10 @@ namespace Avalonia.Win32 private double PrimaryScreenRenderScaling => Screen.AllScreens.FirstOrDefault(screen => screen.IsPrimary)?.Scaling ?? 1; - /// public double RenderScaling => _scaling; - /// public double DesktopScaling => RenderScaling; - /// public Size ClientSize { get @@ -262,7 +250,6 @@ namespace Avalonia.Win32 } } - /// public Size? FrameSize { get @@ -278,18 +265,14 @@ namespace Avalonia.Win32 } } - /// public IScreenImpl Screen { get; } - /// public IPlatformHandle Handle { get; private set; } - /// public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / RenderScaling, _maxTrackSize.Y / RenderScaling); public IMouseDevice MouseDevice => _mouseDevice; - /// public WindowState WindowState { get @@ -327,14 +310,12 @@ namespace Avalonia.Win32 } } - /// public WindowTransparencyLevel TransparencyLevel { get; private set; } protected IntPtr Hwnd => _hwnd; private bool IsMouseInPointerEnabled => _wmPointerEnabled && IsMouseInPointerEnabled(); - /// public object? TryGetFeature(Type featureType) { if (featureType == typeof(ITextInputMethodImpl)) @@ -355,7 +336,6 @@ namespace Avalonia.Win32 return null; } - /// public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); @@ -537,13 +517,11 @@ namespace Avalonia.Win32 } } - /// public IEnumerable Surfaces => _gl is null ? new object[] { Handle, _framebuffer } : new object[] { Handle, _gl, _framebuffer }; - /// public PixelPoint Position { get @@ -589,21 +567,17 @@ namespace Avalonia.Win32 } } - /// public void Move(PixelPoint point) => Position = point; - /// public void SetMinMaxSize(Size minSize, Size maxSize) { _minSize = minSize; _maxSize = maxSize; } - /// public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Win32Platform.Compositor, () => Surfaces); - /// public void Resize(Size value, PlatformResizeReason reason) { if (WindowState != WindowState.Normal) @@ -631,16 +605,13 @@ namespace Avalonia.Win32 } } - /// public void Activate() { SetForegroundWindow(_hwnd); } - /// public IPopupImpl? CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); - /// public void Dispose() { (_gl as IDisposable)?.Dispose(); @@ -682,7 +653,6 @@ namespace Avalonia.Win32 InvalidateRect(_hwnd, ref r, false); } - /// public Point PointToClient(PixelPoint point) { var p = new POINT { X = point.X, Y = point.Y }; @@ -690,7 +660,6 @@ namespace Avalonia.Win32 return new Point(p.X, p.Y) / RenderScaling; } - /// public PixelPoint PointToScreen(Point point) { point *= RenderScaling; @@ -699,31 +668,26 @@ namespace Avalonia.Win32 return new PixelPoint(p.X, p.Y); } - /// public void SetInputRoot(IInputRoot inputRoot) { _owner = inputRoot; CreateDropTarget(inputRoot); } - /// public void Hide() { UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); _shown = false; } - /// public virtual void Show(bool activate, bool isDialog) { SetParent(_parent); ShowWindow(_showWindowState, activate); } - /// public Action? GotInputWhenDisabled { get; set; } - /// public void SetParent(IWindowImpl? parent) { _parent = parent as WindowImpl; @@ -739,10 +703,8 @@ namespace Avalonia.Win32 SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd); } - /// public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); - /// public void BeginMoveDrag(PointerPressedEventArgs e) { e.Pointer.Capture(null); @@ -750,7 +712,6 @@ namespace Avalonia.Win32 new IntPtr((int)HitTestValues.HTCAPTION), IntPtr.Zero); } - /// public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) { if (_windowProperties.IsResizable) @@ -765,13 +726,11 @@ namespace Avalonia.Win32 } } - /// public void SetTitle(string? title) { SetWindowText(_hwnd, title); } - /// public void SetCursor(ICursorImpl? cursor) { var impl = cursor as CursorImpl; @@ -779,13 +738,12 @@ namespace Avalonia.Win32 var hCursor = impl?.Handle ?? s_defaultCursor; SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); - if (_owner?.IsPointerOver == true) + if (Owner.IsPointerOver) { UnmanagedMethods.SetCursor(hCursor); } } - /// public void SetIcon(IWindowIconImpl? icon) { var impl = icon as IconImpl; @@ -795,7 +753,6 @@ namespace Avalonia.Win32 new IntPtr((int)Icons.ICON_BIG), hIcon); } - /// public void ShowTaskbarIcon(bool value) { var newWindowProperties = _windowProperties; @@ -805,7 +762,6 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); } - /// public void CanResize(bool value) { var newWindowProperties = _windowProperties; @@ -815,7 +771,6 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); } - /// public void SetSystemDecorations(SystemDecorations value) { var newWindowProperties = _windowProperties; @@ -825,7 +780,6 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); } - /// public void SetTopmost(bool value) { if (value == _topmost) @@ -842,7 +796,6 @@ namespace Avalonia.Win32 _topmost = value; } - /// public unsafe void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { if (Win32Platform.WindowsVersion.Build >= 22000) @@ -1457,7 +1410,6 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; - /// public void SetExtendClientAreaToDecorationsHint(bool hint) { _isClientAreaExtended = hint; @@ -1465,7 +1417,6 @@ namespace Avalonia.Win32 ExtendClientArea(); } - /// public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { _extendChromeHints = hints; From 19078979e38a9facb0be00ed0daeca3bd53c9e3a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 09:40:42 +0100 Subject: [PATCH 041/108] Initial implementation of SetCurrentValue. --- src/Avalonia.Base/AvaloniaObject.cs | 20 +- .../Diagnostics/AvaloniaPropertyValue.cs | 23 +- .../PropertyStore/EffectiveValue.cs | 6 + .../PropertyStore/EffectiveValue`1.cs | 29 +- src/Avalonia.Base/PropertyStore/ValueStore.cs | 23 +- .../AvaloniaObjectTests_SetCurrentValue.cs | 270 ++++++++++++++++++ 6 files changed, 345 insertions(+), 26 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 74dc55355b..93bbee12b8 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -355,6 +355,23 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } + public void SetCurrentValue(StyledProperty property, T value) + { + _ = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + LogPropertySet(property, value, BindingPriority.LocalValue); + + if (value is UnsetValueType) + { + _values.ClearLocalValue(property); + } + else if (value is not DoNothingType) + { + _values.SetCurrentValue(property, value); + } + } + /// /// Binds a to an observable. /// @@ -547,7 +564,8 @@ namespace Avalonia property, GetValue(property), BindingPriority.LocalValue, - null); + null, + false); } return _values.GetDiagnostic(property); diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs index 4189fd5234..0b3e62f1cc 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs @@ -3,28 +3,23 @@ using Avalonia.Data; namespace Avalonia.Diagnostics { /// - /// Holds diagnostic-related information about the value of a - /// on a . + /// Holds diagnostic-related information about the value of an + /// on an . /// public class AvaloniaPropertyValue { - /// - /// Initializes a new instance of the class. - /// - /// The property. - /// The current property value. - /// The priority of the current value. - /// A diagnostic string. - public AvaloniaPropertyValue( + internal AvaloniaPropertyValue( AvaloniaProperty property, object? value, BindingPriority priority, - string? diagnostic) + string? diagnostic, + bool isOverriddenCurrentValue) { Property = property; Value = value; Priority = priority; Diagnostic = diagnostic; + IsOverriddenCurrentValue = isOverriddenCurrentValue; } /// @@ -46,5 +41,11 @@ namespace Avalonia.Diagnostics /// Gets a diagnostic string. /// public string? Diagnostic { get; } + + /// + /// Gets a value indicating whether the was overridden by a call to + /// . + /// + public bool IsOverriddenCurrentValue { get; } } } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 04d3c805c2..78f0ad46b7 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -29,6 +29,12 @@ namespace Avalonia.PropertyStore /// public BindingPriority BasePriority { get; protected set; } + /// + /// Gets a value indicating whether the was overridden by a call to + /// . + /// + public bool IsOverridenCurrentValue { get; set; } + /// /// Begins a reevaluation pass on the effective value. /// diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 3e20dcce56..0d93e9d8ed 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -57,7 +57,7 @@ namespace Avalonia.PropertyStore Debug.Assert(priority != BindingPriority.LocalValue); UpdateValueEntry(value, priority); - SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority); + SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); } public void SetLocalValueAndRaise( @@ -65,7 +65,16 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); + SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false); + } + + public void SetCurrentValueAndRaise( + ValueStore owner, + StyledProperty property, + T value) + { + IsOverridenCurrentValue = true; + SetAndRaiseCore(owner, property, value, Priority, true); } public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) @@ -98,7 +107,7 @@ namespace Avalonia.PropertyStore Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(BasePriority != BindingPriority.Unset); UpdateValueEntry(null, BindingPriority.Animation); - SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority); + SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false); } public override void CoerceValue(ValueStore owner, AvaloniaProperty property) @@ -158,15 +167,16 @@ namespace Avalonia.PropertyStore ValueStore owner, StyledProperty property, T value, - BindingPriority priority) + BindingPriority priority, + bool isOverriddenCurrentValue) { - Debug.Assert(priority < BindingPriority.Inherited); - var oldValue = Value; var valueChanged = false; var baseValueChanged = false; var v = value; + IsOverridenCurrentValue = isOverriddenCurrentValue; + if (_uncommon?._coerce is { } coerce) v = coerce(owner.Owner, value); @@ -209,7 +219,6 @@ namespace Avalonia.PropertyStore T baseValue, BindingPriority basePriority) { - Debug.Assert(priority < BindingPriority.Inherited); Debug.Assert(basePriority > BindingPriority.Animation); Debug.Assert(priority <= basePriority); @@ -225,7 +234,7 @@ namespace Avalonia.PropertyStore bv = coerce(owner.Owner, baseValue); } - if (priority != BindingPriority.Unset && !EqualityComparer.Default.Equals(Value, v)) + if (!EqualityComparer.Default.Equals(Value, v)) { Value = v; valueChanged = true; @@ -233,9 +242,7 @@ namespace Avalonia.PropertyStore _uncommon._uncoercedValue = value; } - if (priority != BindingPriority.Unset && - (BasePriority == BindingPriority.Unset || - !EqualityComparer.Default.Equals(_baseValue, bv))) + if (!EqualityComparer.Default.Equals(_baseValue, bv)) { _baseValue = v; baseValueChanged = true; diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index a758360545..fd5cd91a6c 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -7,7 +7,6 @@ using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Styling; using Avalonia.Utilities; -using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -159,8 +158,9 @@ namespace Avalonia.PropertyStore public void ClearLocalValue(AvaloniaProperty property) { if (TryGetEffectiveValue(property, out var effective) && - effective.Priority == BindingPriority.LocalValue) + (effective.Priority == BindingPriority.LocalValue || effective.IsOverridenCurrentValue)) { + effective.IsOverridenCurrentValue = false; ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true); } } @@ -209,6 +209,20 @@ namespace Avalonia.PropertyStore } } + public void SetCurrentValue(StyledProperty property, T value) + { + if (_effectiveValues.TryGetValue(property, out var v)) + { + ((EffectiveValue)v).SetCurrentValueAndRaise(this, property, value); + } + else + { + var effectiveValue = new EffectiveValue(Owner, property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetCurrentValueAndRaise(this, property, value); + } + } + public object? GetValue(AvaloniaProperty property) { if (_effectiveValues.TryGetValue(property, out var v)) @@ -616,11 +630,13 @@ namespace Avalonia.PropertyStore { object? value; BindingPriority priority; + bool overridden = false; if (_effectiveValues.TryGetValue(property, out var v)) { value = v.Value; priority = v.Priority; + overridden = v.IsOverridenCurrentValue; } else if (property.Inherits && TryGetInheritedValue(property, out v)) { @@ -637,7 +653,8 @@ namespace Avalonia.PropertyStore property, value, priority, - null); + null, + overridden); } private int InsertFrame(ValueFrame frame) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs new file mode 100644 index 0000000000..16f924acba --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -0,0 +1,270 @@ +using System; +using System.Reactive.Linq; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Reactive; +using Xunit; +using Observable = Avalonia.Reactive.Observable; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_SetCurrentValue + { + [Fact] + public void SetCurrentValue_Sets_Unset_Value() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void SetCurrentValue_Overrides_Existing_Value(BindingPriority priority) + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "oldvalue", priority); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void SetCurrentValue_Overrides_Inherited_Value() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetValue(Class1.InheritedProperty, "inheritedvalue"); + target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.InheritedProperty)); + Assert.True(IsOverridden(target, Class1.InheritedProperty)); + } + + [Fact] + public void SetCurrentValue_Is_Inherited() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.Equal(BindingPriority.Inherited, GetPriority(target, Class1.InheritedProperty)); + Assert.False(IsOverridden(target, Class1.InheritedProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Unset_Priority() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("foodefault", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Inherited_Priority() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetValue(Class1.InheritedProperty, "inheritedvalue"); + target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + target.ClearValue(Class1.InheritedProperty); + + Assert.Equal("inheritedvalue", target.Inherited); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("foodefault", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "stylevalue", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("stylevalue", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void SetCurrentValue_Can_Be_Coerced() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.CoercedProperty, 60); + Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); + + target.CoerceMax = 50; + target.CoerceValue(Class1.CoercedProperty); + Assert.Equal(50, target.GetValue(Class1.CoercedProperty)); + + target.CoerceMax = 100; + target.CoerceValue(Class1.CoercedProperty); + Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void SetValue_Overrides_CurrentValue_With_Unset_Priority(BindingPriority priority) + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", priority); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void Animation_Value_Overrides_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.Animation); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void StyleTrigger_Value_Overrides_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.StyleTrigger); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void Binding_Overrides_CurrentValue_With_Unset_Priority(BindingPriority priority) + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), priority); + + Assert.Equal("binding", target.Foo); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("foodefault", target.Foo); + } + + [Fact] + public void Animation_Binding_Overrides_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.Animation); + + Assert.Equal("binding", target.Foo); + Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("current", target.Foo); + } + + [Fact] + public void StyleTrigger_Binding_Overrides_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.StyleTrigger); + + Assert.Equal("binding", target.Foo); + Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("style", target.Foo); + } + + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) + { + return target.GetDiagnostic(property).Priority; + } + + private bool IsOverridden(AvaloniaObject target, AvaloniaProperty property) + { + return target.GetDiagnostic(property).IsOverriddenCurrentValue; + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register(nameof(Foo), "foodefault"); + public static readonly StyledProperty InheritedProperty = + AvaloniaProperty.Register(nameof(Inherited), "inheriteddefault", inherits: true); + public static readonly StyledProperty CoercedProperty = + AvaloniaProperty.Register(nameof(Coerced), coerce: Coerce); + + public string Foo => GetValue(FooProperty); + public string Inherited => GetValue(InheritedProperty); + public double Coerced => GetValue(CoercedProperty); + public double CoerceMax { get; set; } = 100; + + private static double Coerce(AvaloniaObject sender, double value) + { + return Math.Min(value, ((Class1)sender).CoerceMax); + } + } + } +} From 8741b7e4106fd0ceef9f68ddc5eb3dfae5604e8e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 10:53:03 +0100 Subject: [PATCH 042/108] Fix IsSet with SetCurrentValue. And add unit tests. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 7 +------ .../AvaloniaObjectTests_SetCurrentValue.cs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index fd5cd91a6c..64e3c498e9 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -249,12 +249,7 @@ namespace Avalonia.PropertyStore return false; } - public bool IsSet(AvaloniaProperty property) - { - if (_effectiveValues.TryGetValue(property, out var v)) - return v.Priority < BindingPriority.Inherited; - return false; - } + public bool IsSet(AvaloniaProperty property) => _effectiveValues.TryGetValue(property, out _); public void CoerceValue(AvaloniaProperty property) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 16f924acba..3edf0b105a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,8 +1,6 @@ using System; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; -using Avalonia.Reactive; using Xunit; using Observable = Avalonia.Reactive.Observable; @@ -18,6 +16,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); Assert.True(IsOverridden(target, Class1.FooProperty)); } @@ -34,6 +33,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.True(IsOverridden(target, Class1.FooProperty)); } @@ -48,6 +48,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.True(target.IsSet(Class1.InheritedProperty)); Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.InheritedProperty)); Assert.True(IsOverridden(target, Class1.InheritedProperty)); } @@ -61,6 +62,7 @@ namespace Avalonia.Base.UnitTests parent.SetCurrentValue(Class1.InheritedProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Inherited, GetPriority(target, Class1.InheritedProperty)); Assert.False(IsOverridden(target, Class1.InheritedProperty)); } @@ -74,6 +76,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -88,6 +91,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.InheritedProperty); Assert.Equal("inheritedvalue", target.Inherited); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -101,6 +105,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -114,6 +119,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("stylevalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -146,6 +152,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", priority); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -160,6 +167,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.Animation); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -174,6 +182,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.StyleTrigger); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -191,6 +200,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), priority); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); @@ -210,6 +220,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.Animation); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); @@ -229,6 +240,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.StyleTrigger); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); From 0ce9180d7c56b17c7b6a03c44d82161f6d031036 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 11:41:59 +0100 Subject: [PATCH 043/108] Added untyped SetCurrentValue. --- src/Avalonia.Base/AvaloniaObject.cs | 34 +++++++++++++++++++ src/Avalonia.Base/AvaloniaProperty.cs | 7 ++++ src/Avalonia.Base/DirectPropertyBase.cs | 5 +++ src/Avalonia.Base/StyledProperty.cs | 21 ++++++++++++ .../AvaloniaObjectTests_SetCurrentValue.cs | 26 ++++++++++++++ .../AvaloniaPropertyTests.cs | 5 +++ 6 files changed, 98 insertions(+) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 93bbee12b8..5a5827d0aa 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -355,6 +355,40 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } + /// + /// Sets the value of a dependency property without changing its value source. + /// + /// The property. + /// The value. + /// + /// This method is used by a component that programmatically sets the value of one of its + /// own properties without disabling an application's declared use of the property. The + /// method changes the effective value of the property, but existing data bindings and + /// styles will continue to work. + /// + /// The new value will have the property's current , even if + /// that priority is or + /// . + /// + public void SetCurrentValue(AvaloniaProperty property, object? value) => + property.RouteSetCurrentValue(this, value); + + /// + /// Sets the value of a dependency property without changing its value source. + /// + /// The type of the property. + /// The property. + /// The value. + /// + /// This method is used by a component that programmatically sets the value of one of its + /// own properties without disabling an application's declared use of the property. The + /// method changes the effective value of the property, but existing data bindings and + /// styles will continue to work. + /// + /// The new value will have the property's current , even if + /// that priority is or + /// . + /// public void SetCurrentValue(StyledProperty property, T value) { _ = property ?? throw new ArgumentNullException(nameof(property)); diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 5db4d81f03..1c1d09c3f5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,6 +496,13 @@ namespace Avalonia object? value, BindingPriority priority); + /// + /// Routes an untyped SetCurrentValue call to a typed call. + /// + /// The object instance. + /// The value. + internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value); + /// /// Routes an untyped Bind call to a typed call. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 9ee1eee0fa..94dfaaab01 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -152,6 +152,11 @@ namespace Avalonia return null; } + internal override void RouteSetCurrentValue(AvaloniaObject o, object? value) + { + RouteSetValue(o, value, BindingPriority.LocalValue); + } + /// /// Routes an untyped Bind call to a typed call. /// diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 79d1b9202d..8e0ecf5544 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -220,6 +220,27 @@ namespace Avalonia } } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] + internal override void RouteSetCurrentValue(AvaloniaObject target, object? value) + { + if (value == BindingOperations.DoNothing) + return; + + if (value == UnsetValue) + { + target.ClearValue(this); + } + else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) + { + target.SetCurrentValue(this, (TValue)converted!); + } + else + { + var type = value?.GetType().FullName ?? "(null)"; + throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); + } + } + internal override IDisposable RouteBind( AvaloniaObject target, IObservable source, diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 3edf0b105a..8ad36a583e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -21,6 +21,19 @@ namespace Avalonia.Base.UnitTests Assert.True(IsOverridden(target, Class1.FooProperty)); } + [Fact] + public void SetCurrentValue_Sets_Unset_Value_Untyped() + { + var target = new Class1(); + + target.SetCurrentValue((AvaloniaProperty)Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + [Theory] [InlineData(BindingPriority.LocalValue)] [InlineData(BindingPriority.Style)] @@ -140,6 +153,19 @@ namespace Avalonia.Base.UnitTests Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); } + [Fact] + public void SetCurrentValue_Unset_Clears_CurrentValue() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.SetCurrentValue(Class1.FooProperty, AvaloniaProperty.UnsetValue); + + Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + [Theory] [InlineData(BindingPriority.LocalValue)] [InlineData(BindingPriority.Style)] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 5733159a23..a9b8a5f21b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -179,6 +179,11 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } + internal override void RouteSetCurrentValue(AvaloniaObject o, object value) + { + throw new NotImplementedException(); + } + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) { throw new NotImplementedException(); From 814ae8401117e5549100cb96058124c5438ceb2f Mon Sep 17 00:00:00 2001 From: Mike James Date: Mon, 13 Feb 2023 13:57:43 +0100 Subject: [PATCH 044/108] Updated readme to include XPF banner --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index c2be487af3..2600cf83cc 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ +[![GH_Banner](https://user-images.githubusercontent.com/552074/218457976-92e76834-9e22-4e35-acfa-aa50281bc0f9.png)](https://avaloniaui.net/xpf) + [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
From fc0ddcb320754c8a3da969e32be23073d8202d6e Mon Sep 17 00:00:00 2001 From: kekekeks Date: Mon, 13 Feb 2023 16:19:19 +0300 Subject: [PATCH 045/108] Use NT shared handles for Vulkan interop on Windows --- .../GpuInterop/VulkanDemo/D3DMemoryHelper.cs | 2 +- .../GpuInterop/VulkanDemo/VulkanContext.cs | 2 +- samples/GpuInterop/VulkanDemo/VulkanImage.cs | 30 +++++++++----- .../Avalonia.Win32/DirectX/directx.idl | 40 +++++++++++++++++++ .../Angle/AngleExternalObjectsFeature.cs | 14 +++++-- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs b/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs index a0b7d32d3b..ac09c48ccd 100644 --- a/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs +++ b/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs @@ -47,7 +47,7 @@ public class D3DMemoryHelper MipLevels = 1, SampleDescription = new SampleDescription { Count = 1, Quality = 0 }, CpuAccessFlags = default, - OptionFlags = ResourceOptionFlags.SharedKeyedmutex, + OptionFlags = ResourceOptionFlags.SharedKeyedmutex|ResourceOptionFlags.SharedNthandle, BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource }); } diff --git a/samples/GpuInterop/VulkanDemo/VulkanContext.cs b/samples/GpuInterop/VulkanDemo/VulkanContext.cs index 3fdd9695f2..1d44549089 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanContext.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanContext.cs @@ -173,7 +173,7 @@ public unsafe class VulkanContext : IDisposable api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, familyProperties); for (uint queueFamilyIndex = 0; queueFamilyIndex < queueFamilyCount; queueFamilyIndex++) { - var family = familyProperties[c]; + var family = familyProperties[queueFamilyIndex]; if (!family.QueueFlags.HasAllFlags(QueueFlags.GraphicsBit)) continue; diff --git a/samples/GpuInterop/VulkanDemo/VulkanImage.cs b/samples/GpuInterop/VulkanDemo/VulkanImage.cs index 59b2ef7e30..dfb6ba20e0 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanImage.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanImage.cs @@ -4,10 +4,13 @@ using System.Runtime.InteropServices; using Avalonia; using Avalonia.Platform; using Avalonia.Vulkan; +using SharpDX.DXGI; using Silk.NET.Vulkan; using Silk.NET.Vulkan.Extensions.KHR; using SilkNetDemo; using SkiaSharp; +using Device = Silk.NET.Vulkan.Device; +using Format = Silk.NET.Vulkan.Format; namespace GpuInterop.VulkanDemo; @@ -24,7 +27,6 @@ public unsafe class VulkanImage : IDisposable private ImageView? _imageView { get; set; } private DeviceMemory _imageMemory { get; set; } private SharpDX.Direct3D11.Texture2D? _d3dTexture2D; - private IntPtr _win32ShareHandle; internal Image? InternalHandle { get; private set; } internal Format Format { get; } @@ -108,14 +110,14 @@ public unsafe class VulkanImage : IDisposable if (exportable && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { _d3dTexture2D = D3DMemoryHelper.CreateMemoryHandle(vk.D3DDevice, size, Format); - using var dxgi = _d3dTexture2D.QueryInterface(); - _win32ShareHandle = dxgi.SharedHandle; + using var dxgi = _d3dTexture2D.QueryInterface(); + handleImport = new ImportMemoryWin32HandleInfoKHR { PNext = &dedicatedAllocation, SType = StructureType.ImportMemoryWin32HandleInfoKhr, - HandleType = ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit, - Handle = _win32ShareHandle, + HandleType = ExternalMemoryHandleTypeFlags.D3D11TextureBit, + Handle = dxgi.CreateSharedHandle(null, SharedResourceFlags.Read | SharedResourceFlags.Write), }; } @@ -185,11 +187,19 @@ public unsafe class VulkanImage : IDisposable return fd; } - public IPlatformHandle Export() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? - new PlatformHandle(_win32ShareHandle, - KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) : - new PlatformHandle(new IntPtr(ExportFd()), - KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor); + public IPlatformHandle Export() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using var dxgi = _d3dTexture2D!.QueryInterface(); + return new PlatformHandle( + dxgi.CreateSharedHandle(null, SharedResourceFlags.Read | SharedResourceFlags.Write), + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureNtHandle); + } + else + return new PlatformHandle(new IntPtr(ExportFd()), + KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor); + } public ImageTiling Tiling => ImageTiling.Optimal; diff --git a/src/Windows/Avalonia.Win32/DirectX/directx.idl b/src/Windows/Avalonia.Win32/DirectX/directx.idl index 1d66c898db..120d1d5159 100644 --- a/src/Windows/Avalonia.Win32/DirectX/directx.idl +++ b/src/Windows/Avalonia.Win32/DirectX/directx.idl @@ -36,6 +36,8 @@ @clr-map DXGI_SHARED_RESOURCE void* @clr-map LUID ulong @clr-map LPSTR ushort* +@clr-map LPWSTR ushort* +@clr-map LPCWSTR ushort* enum DXGI_FORMAT @@ -501,6 +503,44 @@ interface ID3D11Device : IUnknown UINT GetExceptionMode(); } +[uuid(a04bfb29-08ef-43d6-a49c-a9bdbdcbe686)] +interface ID3D11Device1 : ID3D11Device +{ + void GetImmediateContext1( void** ppImmediateContext ); + + HRESULT CreateDeferredContext1( + UINT ContextFlags, // Reserved parameter; must be 0 + [out, retval] IUnknown** ppDeferredContext ); + + HRESULT CreateBlendState1( + void* pBlendStateDesc, + [out, retval] IUnknown** ppBlendState ); + + HRESULT CreateRasterizerState1( + void* pRasterizerDesc, + [out, retval] IUnknown** ppRasterizerState ); + + HRESULT CreateDeviceContextState( + UINT Flags, + void* pFeatureLevels, + UINT FeatureLevels, + UINT SDKVersion, + GUID* EmulatedInterface, + void* pChosenFeatureLevel, + [out, retval] IUnknown** ppContextState); + + HRESULT OpenSharedResource1( + IntPtr hResource, + Guid* ReturnedInterface, + [out, retval] IUnknown** ppResource); + + HRESULT OpenSharedResourceByName( + LPCWSTR lpName, + DWORD dwDesiredAccess, + REFIID returnedInterface, + void** ppResource); +}; + [uuid( 6f15aaf2-d208-4e89-9ab4-489535d34f9c)] interface ID3D11Texture2D : IUnknown diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs index a7b52e953e..4349da85c8 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls.Documents; using Avalonia.OpenGL; @@ -15,12 +16,14 @@ internal class AngleExternalObjectsFeature : IGlContextExternalObjectsFeature, I { private readonly EglContext _context; private readonly ID3D11Device _device; + private readonly ID3D11Device1 _device1; public AngleExternalObjectsFeature(EglContext context) { _context = context; var angle = (AngleWin32EglDisplay)context.Display; _device = MicroComRuntime.CreateProxyFor(angle.GetDirect3DDevice(), false).CloneReference(); + _device1 = _device.QueryInterface(); using var dxgiDevice = _device.QueryInterface(); using var adapter = dxgiDevice.Adapter; DeviceLuid = BitConverter.GetBytes(adapter.Desc.AdapterLuid); @@ -28,7 +31,8 @@ internal class AngleExternalObjectsFeature : IGlContextExternalObjectsFeature, I public IReadOnlyList SupportedImportableExternalImageTypes { get; } = new[] { - KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle, + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureNtHandle, }; public IReadOnlyList SupportedExportableExternalImageTypes => SupportedImportableExternalImageTypes; @@ -72,13 +76,17 @@ internal class AngleExternalObjectsFeature : IGlContextExternalObjectsFeature, I public unsafe IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) { - if (handle.HandleDescriptor != KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) + if (!SupportedImportableExternalImageTypes.Contains(handle.HandleDescriptor)) throw new NotSupportedException("Unsupported external memory type"); using (_context.EnsureCurrent()) { var guid = MicroComRuntime.GetGuidFor(typeof(ID3D11Texture2D)); - using var opened = _device.OpenSharedResource(handle.Handle, &guid); + using var opened = + handle.HandleDescriptor == + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle ? + _device.OpenSharedResource(handle.Handle, &guid) : + _device1.OpenSharedResource1(handle.Handle, &guid); using var texture = opened.QueryInterface(); return new AngleExternalMemoryD3D11Texture2D(_context, texture, properties); } From 3e0179e1e033763a7bdbf13af98eaa06b836cf19 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 9 Feb 2023 20:27:29 +0000 Subject: [PATCH 046/108] add composing region to text input client, improves android composition --- .../Avalonia.Android/AndroidInputMethod.cs | 40 ++++-- src/Android/Avalonia.Android/InputEditable.cs | 93 ++++++++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 132 +++--------------- .../Input/TextInput/ITextInputMethodClient.cs | 6 + .../Presenters/TextPresenter.cs | 33 ++++- .../TextBoxTextInputMethodClient.cs | 11 ++ 6 files changed, 192 insertions(+), 123 deletions(-) create mode 100644 src/Android/Avalonia.Android/InputEditable.cs diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index c885a7768c..459b20c410 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -5,8 +5,10 @@ using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Controls.Presenters; using Avalonia.Input; using Avalonia.Input.TextInput; +using Avalonia.Reactive; namespace Avalonia.Android { @@ -39,6 +41,7 @@ namespace Avalonia.Android private readonly InputMethodManager _imm; private ITextInputMethodClient _client; private AvaloniaInputConnection _inputConnection; + private IDisposable _textChangeObservable; public AndroidInputMethod(TView host) { @@ -70,13 +73,9 @@ namespace Avalonia.Android { if (_client != null) { + _textChangeObservable?.Dispose(); _client.SurroundingTextChanged -= SurroundingTextChanged; - } - - if(_inputConnection != null) - { - _inputConnection.ComposingText = null; - _inputConnection.ComposingRegion = default; + _client.TextViewVisualChanged -= TextViewVisualChanged; } _client = client; @@ -84,6 +83,12 @@ namespace Avalonia.Android if (IsActive) { _client.SurroundingTextChanged += SurroundingTextChanged; + _client.TextViewVisualChanged += TextViewVisualChanged; + + if(_client.TextViewVisual is TextPresenter textVisual) + { + _textChangeObservable = textVisual.GetObservable(TextPresenter.TextProperty).Subscribe(new AnonymousObserver(UpdateText)); + } _host.RequestFocus(); @@ -101,6 +106,23 @@ namespace Avalonia.Android } } + private void TextViewVisualChanged(object sender, EventArgs e) + { + var textVisual = _client.TextViewVisual as TextPresenter; + _textChangeObservable?.Dispose(); + _textChangeObservable = null; + + if(textVisual != null) + { + _textChangeObservable = textVisual.GetObservable(TextPresenter.TextProperty).Subscribe(new AnonymousObserver(UpdateText)); + } + } + + private void UpdateText(string? obj) + { + (_inputConnection?.Editable as InputEditable)?.UpdateString(obj); + } + private void SurroundingTextChanged(object sender, EventArgs e) { if (IsActive && _inputConnection != null) @@ -109,12 +131,10 @@ namespace Avalonia.Android _inputConnection.SurroundingText = surroundingText; - _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset); - - if (_inputConnection.ComposingText != null && !_inputConnection.IsCommiting && surroundingText.AnchorOffset == surroundingText.CursorOffset) + if ((_inputConnection?.Editable as InputEditable)?.IsInBatchEdit != true) { - _inputConnection.CommitText(_inputConnection.ComposingText, 0); _inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset); + _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset); } } } diff --git a/src/Android/Avalonia.Android/InputEditable.cs b/src/Android/Avalonia.Android/InputEditable.cs new file mode 100644 index 0000000000..b702b0205c --- /dev/null +++ b/src/Android/Avalonia.Android/InputEditable.cs @@ -0,0 +1,93 @@ +using System; +using Android.Runtime; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Java.Lang; +using static System.Net.Mime.MediaTypeNames; + +namespace Avalonia.Android +{ + internal class InputEditable : SpannableStringBuilder + { + private readonly TopLevelImpl _topLevel; + private readonly IAndroidInputMethod _inputMethod; + private int _currentBatchLevel; + private string _previousText; + + public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) + { + _topLevel = topLevel; + _inputMethod = inputMethod; + } + + public InputEditable(ICharSequence text) : base(text) + { + } + + public InputEditable(string text) : base(text) + { + } + + public InputEditable(ICharSequence text, int start, int end) : base(text, start, end) + { + } + + public InputEditable(string text, int start, int end) : base(text, start, end) + { + } + + protected InputEditable(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public bool IsInBatchEdit => _currentBatchLevel > 0; + + public void BeginBatchEdit() + { + _currentBatchLevel++; + + if(_currentBatchLevel == 1) + { + _previousText = ToString(); + } + } + + public void EndBatchEdit() + { + if (_currentBatchLevel == 1) + { + _inputMethod.Client.SelectInSurroundingText(-1, _previousText.Length); + var time = DateTime.Now.TimeOfDay; + var currentText = ToString(); + + if (string.IsNullOrEmpty(currentText)) + { + _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + } + else + { + var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, currentText); + _topLevel.Input(rawTextEvent); + } + _inputMethod.Client.SelectInSurroundingText(Selection.GetSelectionStart(this), Selection.GetSelectionEnd(this)); + + _previousText = ""; + } + + _currentBatchLevel--; + } + + public void UpdateString(string? text) + { + if(text != ToString()) + { + Clear(); + Insert(0, text); + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 693a26f3bd..b6bcc4ac9f 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -410,27 +410,23 @@ namespace Avalonia.Android.Platform.SkiaPlatform { private readonly TopLevelImpl _topLevel; private readonly IAndroidInputMethod _inputMethod; + private readonly InputEditable _editable; public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) { _topLevel = topLevel; _inputMethod = inputMethod; + _editable = new InputEditable(_topLevel, _inputMethod); } public TextInputMethodSurroundingText SurroundingText { get; set; } - public string ComposingText { get; internal set; } - - public ComposingRegion? ComposingRegion { get; internal set; } - - public bool IsComposing => !string.IsNullOrEmpty(ComposingText); - public bool IsCommiting { get; private set; } + public override IEditable Editable => _editable; public override bool SetComposingRegion(int start, int end) { - //System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {SurroundingText.Text?.Substring(start, end - start)}"); - - ComposingRegion = new ComposingRegion(start, end); + _inputMethod.Client.SetPreeditText(null); + _inputMethod.Client.SetComposingRegion(new Media.TextFormatting.TextRange(start, end)); return base.SetComposingRegion(start, end); } @@ -439,132 +435,46 @@ namespace Avalonia.Android.Platform.SkiaPlatform { var composingText = text.ToString(); - ComposingText = composingText; - - _inputMethod.Client?.SetPreeditText(ComposingText); - - return base.SetComposingText(text, newCursorPosition); - } - - public override bool FinishComposingText() - { - if (!string.IsNullOrEmpty(ComposingText)) + if (string.IsNullOrEmpty(composingText)) { - CommitText(ComposingText, ComposingText.Length); + return CommitText(text, newCursorPosition); } else { - ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset); + return base.SetComposingText(text, newCursorPosition); } - - return base.FinishComposingText(); } - public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags) + public override bool BeginBatchEdit() { - if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0) - { - var start = System.Math.Max(SurroundingText.CursorOffset - length, 0); - - var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset); - - var text = SurroundingText.Text.Substring(start, end - start); + _editable.BeginBatchEdit(); - //System.Diagnostics.Debug.WriteLine($"Text Before: {text}"); - - return new Java.Lang.String(text); - } - - return null; + return base.BeginBatchEdit(); } - public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags) + public override bool EndBatchEdit() { - if (!string.IsNullOrEmpty(SurroundingText.Text)) - { - var start = SurroundingText.CursorOffset; - - var end = System.Math.Min(start + length, SurroundingText.Text.Length); - - var text = SurroundingText.Text.Substring(start, end - start); - - //System.Diagnostics.Debug.WriteLine($"Text After: {text}"); + var ret = base.EndBatchEdit(); + _editable.EndBatchEdit(); - return new Java.Lang.String(text); - } + return ret; + } - return null; + public override bool FinishComposingText() + { + _inputMethod.Client?.SetComposingRegion(null); + return base.FinishComposingText(); } public override bool CommitText(ICharSequence text, int newCursorPosition) { - IsCommiting = true; - var committedText = text.ToString(); - _inputMethod.Client.SetPreeditText(null); - int? start, end; - - if(SurroundingText.CursorOffset != SurroundingText.AnchorOffset) - { - start = Math.Min(SurroundingText.CursorOffset, SurroundingText.AnchorOffset); - end = Math.Max(SurroundingText.CursorOffset, SurroundingText.AnchorOffset); - } - else if (ComposingRegion != null) - { - start = ComposingRegion?.Start; - end = ComposingRegion?.End; - - ComposingRegion = null; - } - else - { - start = end = _inputMethod.Client.SurroundingText.CursorOffset; - } - - _inputMethod.Client.SelectInSurroundingText((int)start, (int)end); - - var time = DateTime.Now.TimeOfDay; - - var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, committedText); - - _topLevel.Input(rawTextEvent); - - ComposingText = null; - - ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition); + _inputMethod.Client?.SetComposingRegion(null); return base.CommitText(text, newCursorPosition); } - public override bool DeleteSurroundingText(int beforeLength, int afterLength) - { - var surroundingText = _inputMethod.Client.SurroundingText; - - var selectionStart = surroundingText.CursorOffset; - - _inputMethod.Client.SelectInSurroundingText(selectionStart - beforeLength, selectionStart + afterLength); - - _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - - surroundingText = _inputMethod.Client.SurroundingText; - - selectionStart = surroundingText.CursorOffset; - - ComposingRegion = new ComposingRegion(selectionStart, selectionStart); - - return base.DeleteSurroundingText(beforeLength, afterLength); - } - - public override bool SetSelection(int start, int end) - { - _inputMethod.Client.SelectInSurroundingText(start, end); - - ComposingRegion = new ComposingRegion(start, end); - - return base.SetSelection(start, end); - } - public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) { switch (actionCode) diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs index 531cf3c704..8239bc6a21 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media.TextFormatting; using Avalonia.VisualTree; namespace Avalonia.Input.TextInput @@ -30,6 +31,11 @@ namespace Avalonia.Input.TextInput /// void SetPreeditText(string? text); + /// + /// Sets the current composing region. This doesn't remove the composing text from the commited text. + /// + void SetComposingRegion(TextRange? region); + /// /// Indicates if text input client is capable of providing the text around the cursor /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 3481b1ecf3..bb6b03d59a 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -63,6 +63,15 @@ namespace Avalonia.Controls.Presenters o => o.PreeditText, (o, v) => o.PreeditText = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty CompositionRegionProperty = + AvaloniaProperty.RegisterDirect( + nameof(CompositionRegion), + o => o.CompositionRegion, + (o, v) => o.CompositionRegion = v); + /// /// Defines the property. /// @@ -106,6 +115,7 @@ namespace Avalonia.Controls.Presenters private Rect _caretBounds; private Point _navigationPosition; private string? _preeditText; + private TextRange? _compositionRegion; static TextPresenter() { @@ -146,6 +156,12 @@ namespace Avalonia.Controls.Presenters set => SetAndRaise(PreeditTextProperty, ref _preeditText, value); } + public TextRange? CompositionRegion + { + get => _compositionRegion; + set => SetAndRaise(CompositionRegionProperty, ref _compositionRegion, value); + } + /// /// Gets or sets the font family. /// @@ -548,7 +564,20 @@ namespace Avalonia.Controls.Presenters var foreground = Foreground; - if (!string.IsNullOrEmpty(_preeditText)) + if(_compositionRegion != null) + { + var preeditHighlight = new ValueSpan(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0, + new GenericTextRunProperties(typeface, FontSize, + foregroundBrush: foreground, + textDecorations: TextDecorations.Underline)); + + textStyleOverrides = new[] + { + preeditHighlight + }; + + } + else if (!string.IsNullOrEmpty(_preeditText)) { var preeditHighlight = new ValueSpan(_caretIndex, _preeditText.Length, new GenericTextRunProperties(typeface, FontSize, @@ -911,6 +940,7 @@ namespace Avalonia.Controls.Presenters break; } + case nameof(CompositionRegion): case nameof(Foreground): case nameof(FontSize): case nameof(FontStyle): @@ -931,7 +961,6 @@ namespace Avalonia.Controls.Presenters case nameof(PasswordChar): case nameof(RevealPassword): - case nameof(FlowDirection): { InvalidateTextLayout(); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 10c2f36f43..239501aace 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -5,6 +5,7 @@ using Avalonia.Media.TextFormatting; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.VisualTree; +using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls { @@ -110,6 +111,16 @@ namespace Avalonia.Controls _presenter.PreeditText = text; } + public void SetComposingRegion(TextRange? region) + { + if (_presenter == null) + { + return; + } + + _presenter.CompositionRegion = region; + } + public void SelectInSurroundingText(int start, int end) { if(_parent is null ||_presenter is null) From 4edc10caba109f2dcaf669a448c056e1f99f81e2 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 11 Feb 2023 09:26:22 +0000 Subject: [PATCH 047/108] fix prediction text for browser --- src/Browser/Avalonia.Browser/AvaloniaView.cs | 31 +++++++++++++++++++ .../Avalonia.Browser/Interop/InputHelper.cs | 2 ++ .../webapp/modules/avalonia/input.ts | 20 ++++++++++++ 3 files changed, 53 insertions(+) diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 294216ee03..16c2b2a0b4 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices.JavaScript; using Avalonia.Browser.Interop; @@ -15,6 +16,7 @@ using Avalonia.Platform; using Avalonia.Rendering.Composition; using Avalonia.Threading; using SkiaSharp; +using static System.Runtime.CompilerServices.RuntimeHelpers; namespace Avalonia.Browser { @@ -94,6 +96,7 @@ namespace Avalonia.Browser InputHelper.SubscribeTextEvents( _inputElement, + OnBeforeInput, OnTextInput, OnCompositionStart, OnCompositionUpdate, @@ -316,11 +319,37 @@ namespace Avalonia.Browser return _topLevelImpl.RawTextEvent(data); } + private bool OnBeforeInput(JSObject arg, int start, int end) + { + var type = arg.GetPropertyAsString("inputType"); + Console.WriteLine(type); + if (type != "deleteByComposition") + { + if (type == "deleteContentBackward") + { + start = _inputElement.GetPropertyAsInt32("selectionStart"); + end = _inputElement.GetPropertyAsInt32("selectionEnd"); + } + else + { + start = -1; + end = -1; + } + } + + if(start != -1 && end != -1 && _client != null) + { + _client.SelectInSurroundingText(start, end); + } + return false; + } + private bool OnCompositionStart (JSObject args) { if (_client == null) return false; + Console.WriteLine("composition start"); _client.SetPreeditText(null); IsComposing = true; @@ -331,6 +360,7 @@ namespace Avalonia.Browser { if (_client == null) return false; + Console.WriteLine("composition update"); _client.SetPreeditText(args.GetPropertyAsString("data")); @@ -342,6 +372,7 @@ namespace Avalonia.Browser if (_client == null) return false; + Console.WriteLine("composition end"); IsComposing = false; _client.SetPreeditText(null); _topLevelImpl.RawTextEvent(args.GetPropertyAsString("data")!); diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index 7a010dc782..a816e39da8 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -18,6 +18,8 @@ internal static partial class InputHelper [JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeTextEvents( JSObject htmlElement, + [JSMarshalAs>] + Func onBeforeInput, [JSMarshalAs>] Func onInput, [JSMarshalAs>] diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index 83e8ee7f1c..0f0e5eb512 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -47,6 +47,7 @@ export class InputHelper { public static subscribeTextEvents( element: HTMLInputElement, + beforeInputCallback: (args: InputEvent, start: number, end: number) => boolean, inputCallback: (type: string, data: string | null) => boolean, compositionStartCallback: (args: CompositionEvent) => boolean, compositionUpdateCallback: (args: CompositionEvent) => boolean, @@ -68,6 +69,25 @@ export class InputHelper { }; element.addEventListener("compositionstart", compositionStartHandler); + const beforeInputHandler = (args: InputEvent) => { + const ranges = args.getTargetRanges(); + let start = -1; + let end = -1; + if (ranges.length > 0) { + start = ranges[0].startOffset; + end = ranges[0].endOffset; + } + + if (args.inputType === "insertCompositionText") { + start = 2; + end = start + 2; + } + if (beforeInputCallback(args, start, end)) { + args.preventDefault(); + } + }; + element.addEventListener("beforeinput", beforeInputHandler); + const compositionUpdateHandler = (args: CompositionEvent) => { if (compositionUpdateCallback(args)) { args.preventDefault(); From ff983442470f5f851da9eebc8e83b1bf14393ad9 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 13 Feb 2023 08:39:34 +0000 Subject: [PATCH 048/108] allow multiline input on android --- samples/MobileSandbox/MainView.xaml | 4 ++-- src/Android/Avalonia.Android/AndroidInputMethod.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/MobileSandbox/MainView.xaml b/samples/MobileSandbox/MainView.xaml index 1eab13aa75..5d35ec3fec 100644 --- a/samples/MobileSandbox/MainView.xaml +++ b/samples/MobileSandbox/MainView.xaml @@ -5,8 +5,8 @@ x:DataType="mobileSandbox:MainView"> - - + +