From b71743e9a8bb483ee9b2e0112ab37ac258606b48 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Jul 2021 13:28:10 +0200 Subject: [PATCH 01/16] Handle applicationShouldTerminate on OSX. Added some new plumbing to facilitate this: - A managed event is raised `IPlatformLifetimeEventsImpl.ShutdownRequested` - This event is subscribed from `ClassicDesktopStyleApplicationLifetime` - Which tries to close all windows - And returns false to cancel the OS-level close if not all windows could be closed --- native/Avalonia.Native/src/OSX/app.mm | 6 +++++ .../ClassicDesktopStyleApplicationLifetime.cs | 22 ++++++++++++++++++- .../Platform/IPlatformLifetimeEventsImpl.cs | 16 ++++++++++++++ .../AvaloniaNativeApplicationPlatform.cs | 13 ++++++++++- src/Avalonia.Native/AvaloniaNativePlatform.cs | 3 ++- src/Avalonia.Native/avn.idl | 1 + 6 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 460c24ea3a..e1972b22f4 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -50,6 +50,12 @@ ComPtr _events; _events->FilesOpened(array); } + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender +{ + return _events->TryShutdown() ? NSTerminateNow : NSTerminateCancel; +} + @end @interface AvnApplication : NSApplication diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index aa4342f075..872820e420 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading; using Avalonia.Controls; @@ -42,7 +43,7 @@ namespace Avalonia.Controls.ApplicationLifetimes "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); _activeLifetime = this; } - + /// public event EventHandler Startup; /// @@ -111,6 +112,11 @@ namespace Avalonia.Controls.ApplicationLifetimes ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); } + var lifetimeEvents = AvaloniaLocator.Current.GetService(); + + if (lifetimeEvents != null) + lifetimeEvents.ShutdownRequested += ShutdownRequested; + _cts = new CancellationTokenSource(); MainWindow?.Show(); Dispatcher.UIThread.MainLoop(_cts.Token); @@ -123,6 +129,20 @@ namespace Avalonia.Controls.ApplicationLifetimes if (_activeLifetime == this) _activeLifetime = null; } + + private void ShutdownRequested(object sender, CancelEventArgs e) + { + foreach (var w in Windows.ToArray()) + w.Close(); + if (Windows.Count > 0) + e.Cancel = true; + } + + private bool TryCloseAllWindows() + { + + return Windows.Count == 0; + } } public class ClassicDesktopStyleApplicationLifetimeOptions diff --git a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs new file mode 100644 index 0000000000..8e660777e9 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel; + +namespace Avalonia.Platform +{ + public interface IPlatformLifetimeEventsImpl + { + /// + /// Raised by the platform when a shutdown is requested. + /// + /// + /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. + /// + event EventHandler ShutdownRequested; + } +} diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index 9579aa93b1..77c0794d04 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -1,14 +1,25 @@ using System; +using System.ComponentModel; using Avalonia.Native.Interop; using Avalonia.Platform; namespace Avalonia.Native { - internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents + internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl { + public event EventHandler ShutdownRequested; + void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) { ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray()); } + + public int TryShutdown() + { + if (ShutdownRequested is null) return 1; + var e = new CancelEventArgs(); + ShutdownRequested(this, e); + return (!e.Cancel).AsComBool(); + } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 82a845ffc1..60c0b36891 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -111,7 +111,8 @@ namespace Avalonia.Native .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) - .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)); + .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) + .Bind().ToConstant(applicationPlatform); if (_options.UseGpu) { diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index adcbeb2d3a..3071333c5a 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -732,4 +732,5 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown interface IAvnApplicationEvents : IUnknown { void FilesOpened (IAvnStringArray* urls); + bool TryShutdown(); } From bf210cb5f429ccc6507b41d43b5ca120957016c8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Jul 2021 13:40:48 +0200 Subject: [PATCH 02/16] Call terminate from Quit menu. Unifies the closing logic between clicking on the icon menu Quit and the Quit menu. --- native/Avalonia.Native/src/OSX/window.mm | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index c0936356d2..e76d8c4a7e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1887,18 +1887,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent +(void)closeAll { - NSArray* windows = [NSArray arrayWithArray:[NSApp windows]]; - auto numWindows = [windows count]; - - for(int i = 0; i < numWindows; i++) - { - auto window = (AvnWindow*)[windows objectAtIndex:i]; - - if([window parentWindow] == nullptr) // Avalonia will handle the child windows. - { - [window performClose:nil]; - } - } + [[NSApplication sharedApplication] terminate:self]; } - (void)performClose:(id)sender From a9f1781da9fddeaa8079e79ef6b01e2221d6d8fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Jul 2021 14:04:32 +0200 Subject: [PATCH 03/16] Remove dead code. --- .../ClassicDesktopStyleApplicationLifetime.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 872820e420..050ed59583 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -137,12 +137,6 @@ namespace Avalonia.Controls.ApplicationLifetimes if (Windows.Count > 0) e.Cancel = true; } - - private bool TryCloseAllWindows() - { - - return Windows.Count == 0; - } } public class ClassicDesktopStyleApplicationLifetimeOptions From 3ec9b65cc3ac5aee1770e470a2ff2a89831ecb50 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Jul 2021 15:11:57 +0200 Subject: [PATCH 04/16] Remove unneeded list copy. --- .../ClassicDesktopStyleApplicationLifetime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 050ed59583..2c43f13c82 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -132,7 +132,7 @@ namespace Avalonia.Controls.ApplicationLifetimes private void ShutdownRequested(object sender, CancelEventArgs e) { - foreach (var w in Windows.ToArray()) + foreach (var w in Windows) w.Close(); if (Windows.Count > 0) e.Cancel = true; From cee1a24e0330d4daedea663e858ee33d761d2227 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 7 Jul 2021 17:06:59 +0200 Subject: [PATCH 05/16] Only try to close non-owned windows on shutdown. Owned windows will be closed by their owners. --- .../ClassicDesktopStyleApplicationLifetime.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 2c43f13c82..2256f4cb54 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -132,8 +132,12 @@ namespace Avalonia.Controls.ApplicationLifetimes private void ShutdownRequested(object sender, CancelEventArgs e) { + // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel + // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their + // owners. foreach (var w in Windows) - w.Close(); + if (w.Owner is null) + w.Close(); if (Windows.Count > 0) e.Cancel = true; } From 39394bceb6988571ceacd7f4fb85e142d41b5965 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 9 Jul 2021 09:29:58 +0200 Subject: [PATCH 06/16] Don't try to shutdown if e.Cancel already set. This allows users to add their own `ShutdownRequested` handlers which can override the lifetime's handler. --- .../ClassicDesktopStyleApplicationLifetime.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 2256f4cb54..65f1f0fff2 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -132,6 +132,9 @@ namespace Avalonia.Controls.ApplicationLifetimes private void ShutdownRequested(object sender, CancelEventArgs e) { + if (e.Cancel) + return; + // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their // owners. From 298dd4e2997b9aab1f0f203f210355ee7e354197 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 12 Jul 2021 17:36:45 +0200 Subject: [PATCH 07/16] Added ShutdownRequested to IClassicDesktopStyleApplicationLifetime. --- .../ClassicDesktopStyleApplicationLifetime.cs | 10 +++++-- ...IClassicDesktopStyleApplicationLifetime.cs | 12 ++++++++ .../DesktopStyleApplicationLifetimeTests.cs | 30 ++++++++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 65f1f0fff2..79780dbd0b 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -46,6 +46,10 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public event EventHandler Startup; + + /// + public event EventHandler ShutdownRequested; + /// public event EventHandler Exit; @@ -115,7 +119,7 @@ namespace Avalonia.Controls.ApplicationLifetimes var lifetimeEvents = AvaloniaLocator.Current.GetService(); if (lifetimeEvents != null) - lifetimeEvents.ShutdownRequested += ShutdownRequested; + lifetimeEvents.ShutdownRequested += OnShutdownRequested; _cts = new CancellationTokenSource(); MainWindow?.Show(); @@ -130,8 +134,10 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } - private void ShutdownRequested(object sender, CancelEventArgs e) + private void OnShutdownRequested(object sender, CancelEventArgs e) { + ShutdownRequested?.Invoke(this, e); + if (e.Cancel) return; diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 212f0b8617..ecf8a0358f 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; namespace Avalonia.Controls.ApplicationLifetimes { @@ -34,5 +35,16 @@ namespace Avalonia.Controls.ApplicationLifetimes Window MainWindow { get; set; } IReadOnlyList Windows { get; } + + /// + /// Raised by the platform when a shutdown is requested. + /// + /// + /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. This event + /// provides a first-chance to cancel application shutdown; if shutdown is not canceled at this point the application + /// will try to close each non-owned open window, invoking the event on each and allowing + /// each window to cancel the shutdown. + /// + event EventHandler ShutdownRequested; } } diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 84f02aeda5..38713834c3 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Threading; @@ -209,6 +210,33 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(lifetime.Windows); } } + + [Fact] + public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var lifetimeEvents = new Mock(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(lifetimeEvents.Object); + lifetime.Start(Array.Empty()); + + var window = new Window(); + var raised = 0; + + window.Show(); + + lifetime.ShutdownRequested += (s, e) => + { + e.Cancel = true; + ++raised; + }; + + lifetimeEvents.Raise(x => x.ShutdownRequested += null, new CancelEventArgs()); + + Assert.Equal(1, raised); + Assert.Equal(new[] { window }, lifetime.Windows); + } + } } - } From 70c207da994e7fe3cd2d6f1c2852e5143be3d16a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 12 Jul 2021 17:38:34 +0200 Subject: [PATCH 08/16] Update ApiCompatBaseline.txt --- src/Avalonia.Controls/ApiCompatBaseline.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 1abf4fd6ff..596b17c943 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -6,12 +6,15 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Control InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. -Total Issues: 15 +Total Issues: 18 From e69af3fa212017b317f4ca16120c85b038082a69 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 12 Jul 2021 17:43:27 +0200 Subject: [PATCH 09/16] Undo changes to AppCompatBaseline.txt --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index c917902dc3..39a4c3004c 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -5,6 +5,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -73,4 +74,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 74 +Total Issues: 75 From c01c9ae7b662342b653bda4d01a720c57ae886e0 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 19 Jul 2021 12:19:57 +0200 Subject: [PATCH 10/16] Disable text search by default. Only ComboBox needs it enabled by default. --- src/Avalonia.Controls/ComboBox.cs | 1 + src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 89cfb5fa8f..57c07916db 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -91,6 +91,7 @@ namespace Avalonia.Controls FocusableProperty.OverrideDefaultValue(true); SelectedItemProperty.Changed.AddClassHandler((x,e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); + IsTextSearchEnabledProperty.OverrideDefaultValue(true); } /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2fd08ef77c..336ff711c0 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); /// /// Event that should be raised by items that implement to From 0c80f885b764869c6a544f148efb3503d50d9218 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 19 Jul 2021 12:56:17 +0200 Subject: [PATCH 11/16] Implement indexed sort for visual children. --- .../Rendering/SceneGraph/SceneBuilder.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index f2a09b815e..43246c8e9d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Threading; @@ -258,14 +259,25 @@ namespace Avalonia.Rendering.SceneGraph } else if (visualChildren.Count > 1) { - var sortedChildren = new IVisual[visualChildren.Count]; - visualChildren.CopyTo(sortedChildren, 0); + var count = visualChildren.Count; + var sortedChildren = new (IVisual visual, int index)[count]; - Array.Sort(sortedChildren, ZIndexComparer.ComparisonInstance); + for (var i = 0; i < count; i++) + { + sortedChildren[i] = (visualChildren[i], i); + } + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + Array.Sort(sortedChildren, (lhs, rhs) => + { + var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); + + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); foreach (var child in sortedChildren) { - var childNode = GetOrCreateChildNode(scene, child, node); + var childNode = GetOrCreateChildNode(scene, child.Item1, node); Update(context, scene, (VisualNode)childNode, clip, forceRecurse); } } From c598b3883adb6f6908173ff18ca83465335955e3 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 19 Jul 2021 13:12:28 +0200 Subject: [PATCH 12/16] Add unit test that verifies ordering. --- .../Rendering/SceneGraph/SceneBuilderTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 42e573c8a5..e317b43efc 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -245,6 +245,34 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Should_Respect_Uniform_ZIndex() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Panel panel; + + var tree = new TestRoot + { + Child = panel = new Panel() + }; + + for (var i = 0; i < 128; i++) + { + panel.Children.Add(new Border()); + } + + var result = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(result); + + var panelNode = result.FindNode(tree.Child); + var expected = panel.Children.ToArray(); + var actual = panelNode.Children.OfType().Select(x => x.Visual).ToArray(); + Assert.Equal(expected, actual); + } + } + [Fact] public void ClipBounds_Should_Be_In_Global_Coordinates() { From 13a244fed53c36c091cb87d9fd1478dc8603d32c Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 19 Jul 2021 13:14:17 +0200 Subject: [PATCH 13/16] Remove usage. --- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 43246c8e9d..c6cdf474bb 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Threading; From c45b5c042b8a0e28c6aaf34af9f0cd8bc0e93db5 Mon Sep 17 00:00:00 2001 From: dvdsvdv vrfsv Date: Mon, 19 Jul 2021 21:10:05 +0300 Subject: [PATCH 14/16] fix --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 0c4f8249ce..f2dbf42196 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -6,8 +6,8 @@ 0.6 0.8 - M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z - M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z + M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z + M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z From 87ea2915f8e335daf2d96fd2993b4703e1ca87d6 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 19:55:05 +0200 Subject: [PATCH 15/16] fixes(DataGrid): Fix binding IsVisible --- .../DataGridColumn.cs | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 9141fb2463..5c09bab678 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -27,7 +27,6 @@ namespace Avalonia.Controls private double? _minWidth; private bool _settingWidthInternally; private int _displayIndexWithFiller; - private bool _isVisible; private object _header; private DataGridColumnHeader _headerCell; private IControl _editingElement; @@ -40,7 +39,6 @@ namespace Avalonia.Controls /// protected internal DataGridColumn() { - _isVisible = true; _displayIndexWithFiller = -1; IsInitialDesiredWidthDetermined = false; InheritsWidth = true; @@ -174,32 +172,42 @@ namespace Avalonia.Controls get => _editBinding; } + + /// + /// Defines the property. + /// + public static StyledProperty IsVisibleProperty = + Control.IsVisibleProperty.AddOwner(); + /// /// Determines whether or not this column is visible. /// public bool IsVisible { - get - { - return _isVisible; - } - set - { - if (value != IsVisible) - { - OwningGrid?.OnColumnVisibleStateChanging(this); - _isVisible = value; + get => GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } - if (_headerCell != null) - { - _headerCell.IsVisible = value; - } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); - OwningGrid?.OnColumnVisibleStateChanged(this); + if (change.Property == IsVisibleProperty) + { + OwningGrid?.OnColumnVisibleStateChanging(this); + var isVisible = (change as AvaloniaPropertyChangedEventArgs).NewValue.Value; + + if (_headerCell != null) + { + _headerCell.IsVisible = isVisible; } + + OwningGrid?.OnColumnVisibleStateChanged(this); + NotifyPropertyChanged(change.Property.Name); } } + /// /// Actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level /// have been taken into account From b9fcee5c3fd6218c8bb7e97a801926406c90bb0b Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 20:01:53 +0200 Subject: [PATCH 16/16] Sample: Add sample for binding DataGridColumn IsVisible --- .../ControlCatalog/Pages/DataGridPage.xaml | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 323eaa3463..820c1324e3 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -23,16 +23,21 @@ - - - - - - - - - - + + + + + + + + + + + + +