From 7240127176e49ff1df4ebaee5434c814037cefa5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Jul 2021 21:20:47 +0100 Subject: [PATCH] Merge pull request #6174 from AvaloniaUI/feature/2736-applicationShouldTerminate OSX: Handle applicationShouldTerminate # Conflicts: # src/Avalonia.Controls/ApiCompatBaseline.txt --- native/Avalonia.Native/src/OSX/app.mm | 6 ++++ native/Avalonia.Native/src/OSX/window.mm | 13 +------- src/Avalonia.Controls/ApiCompatBaseline.txt | 5 +++- .../ClassicDesktopStyleApplicationLifetime.cs | 29 +++++++++++++++++- ...IClassicDesktopStyleApplicationLifetime.cs | 12 ++++++++ .../Platform/IPlatformLifetimeEventsImpl.cs | 16 ++++++++++ .../AvaloniaNativeApplicationPlatform.cs | 13 +++++++- src/Avalonia.Native/AvaloniaNativePlatform.cs | 3 +- src/Avalonia.Native/avn.idl | 1 + .../DesktopStyleApplicationLifetimeTests.cs | 30 ++++++++++++++++++- 10 files changed, 111 insertions(+), 17 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/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 diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 3a6810eed9..11b1f4a833 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,10 +1,13 @@ Compat issues with assembly Avalonia.Controls: InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not 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 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. -Total Issues: 7 +Total Issues: 11 diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index aa4342f075..79780dbd0b 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,9 +43,13 @@ 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; + + /// + public event EventHandler ShutdownRequested; + /// public event EventHandler Exit; @@ -111,6 +116,11 @@ namespace Avalonia.Controls.ApplicationLifetimes ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); } + var lifetimeEvents = AvaloniaLocator.Current.GetService(); + + if (lifetimeEvents != null) + lifetimeEvents.ShutdownRequested += OnShutdownRequested; + _cts = new CancellationTokenSource(); MainWindow?.Show(); Dispatcher.UIThread.MainLoop(_cts.Token); @@ -123,6 +133,23 @@ namespace Avalonia.Controls.ApplicationLifetimes if (_activeLifetime == this) _activeLifetime = null; } + + private void OnShutdownRequested(object sender, CancelEventArgs e) + { + ShutdownRequested?.Invoke(this, 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. + foreach (var w in Windows) + if (w.Owner is null) + w.Close(); + if (Windows.Count > 0) + e.Cancel = true; + } } public class ClassicDesktopStyleApplicationLifetimeOptions 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/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(); } 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); + } + } } - }