From b556a7e4df11a81b7b51b21fd3ab861a93b7ab26 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 20 Jan 2022 12:46:45 -0500 Subject: [PATCH] Merge pull request #7400 from AvaloniaUI/fixes/application-shutdown-osx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix ClassicDesktop Lifetime so that ShutdownRequested event is raised… # Conflicts: # src/Avalonia.Controls/ApiCompatBaseline.txt --- src/Avalonia.Controls/ApiCompatBaseline.txt | 3 +- .../ClassicDesktopStyleApplicationLifetime.cs | 96 ++++++---- ...IClassicDesktopStyleApplicationLifetime.cs | 6 + .../AvaloniaNativeMenuExporter.cs | 8 +- .../DesktopStyleApplicationLifetimeTests.cs | 176 +++++++++++++++++- 5 files changed, 243 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 241d420927a..f1d71c532ab 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -7,6 +7,7 @@ CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not i 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. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.TryShutdown(System.Int32)' is present in the implementation but not in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. @@ -33,4 +34,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 34 +Total Issues: 35 diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 3a2fd68af54..edddf31d45d 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -76,36 +76,21 @@ private void HandleWindowClosed(Window window) return; if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0) - Shutdown(); - else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) - Shutdown(); + TryShutdown(); + else if (ShutdownMode == ShutdownMode.OnMainWindowClose && ReferenceEquals(window, MainWindow)) + TryShutdown(); } public void Shutdown(int exitCode = 0) { - if (_isShuttingDown) - throw new InvalidOperationException("Application is already shutting down."); - - _exitCode = exitCode; - _isShuttingDown = true; + DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode); + } - try - { - foreach (var w in Windows) - w.Close(); - var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); - Exit?.Invoke(this, e); - _exitCode = e.ApplicationExitCode; - } - finally - { - _cts?.Cancel(); - _cts = null; - _isShuttingDown = false; - } + public bool TryShutdown(int exitCode = 0) + { + return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode); } - public int Start(string[] args) { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); @@ -114,7 +99,10 @@ public int Start(string[] args) if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0) { - ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); + if (Application.Current is IApplicationPlatformEvents events) + { + events.RaiseUrlsOpened(args); + } } var lifetimeEvents = AvaloniaLocator.Current.GetService(); @@ -145,23 +133,57 @@ public void Dispose() if (_activeLifetime == this) _activeLifetime = null; } - - private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) + + private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0) { - ShutdownRequested?.Invoke(this, e); + if (!force) + { + ShutdownRequested?.Invoke(this, e); - if (e.Cancel) - return; + if (e.Cancel) + return false; + + if (_isShuttingDown) + throw new InvalidOperationException("Application is already shutting down."); + } + + _exitCode = exitCode; + _isShuttingDown = true; - // 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; + try + { + // 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 (!force && Windows.Count > 0) + { + e.Cancel = true; + return false; + } + + var args = new ControlledApplicationLifetimeExitEventArgs(exitCode); + Exit?.Invoke(this, args); + _exitCode = args.ApplicationExitCode; + } + finally + { + _cts?.Cancel(); + _cts = null; + _isShuttingDown = false; + } + + return true; } + + private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e); } public class ClassicDesktopStyleApplicationLifetimeOptions diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index a70d5dd2f18..a83229b732f 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime { + /// + /// Tries to Shutdown the application. event can be used to cancel the shutdown. + /// + /// An integer exit code for an application. The default exit code is 0. + bool TryShutdown(int exitCode = 0); + /// /// Gets the arguments passed to the /// diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 639c5ba4321..a1e22f0565a 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -131,9 +131,13 @@ private void PopulateStandardOSXMenuItems(NativeMenu appMenu) var quitItem = new NativeMenuItem("Quit") { Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta) }; quitItem.Click += (_, _) => { - if (Application.Current is { ApplicationLifetime: IControlledApplicationLifetime lifetime }) + if (Application.Current is { ApplicationLifetime: IClassicDesktopStyleApplicationLifetime lifetime }) { - lifetime.Shutdown(); + lifetime.TryShutdown(); + } + else if(Application.Current is {ApplicationLifetime: IControlledApplicationLifetime controlledLifetime}) + { + controlledLifetime.Shutdown(); } }; diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index f7a3bdea1c0..3a2e1c08bd0 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; -using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -57,7 +55,7 @@ public void Should_Only_Exit_On_Explicit_Exit() var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -91,7 +89,7 @@ public void Should_Exit_After_MainWindow_Closed() var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var mainWindow = new Window(); @@ -119,7 +117,7 @@ public void Should_Exit_After_Last_Window_Closed() var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -226,7 +224,7 @@ public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event() window.Show(); - lifetime.ShutdownRequested += (s, e) => + lifetime.ShutdownRequested += (_, e) => { e.Cancel = true; ++raised; @@ -238,5 +236,171 @@ public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event() Assert.Equal(new[] { window }, lifetime.Windows); } } + + [Fact] + public void MainWindow_Closed_Shutdown_Should_Be_Cancellable() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; + + var hasExit = false; + + lifetime.Exit += (_, _) => hasExit = true; + + var mainWindow = new Window(); + + mainWindow.Show(); + + lifetime.MainWindow = mainWindow; + + var window = new Window(); + + window.Show(); + + var raised = 0; + + lifetime.ShutdownRequested += (_, e) => + { + e.Cancel = true; + ++raised; + }; + + mainWindow.Close(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } + + [Fact] + public void LastWindow_Closed_Shutdown_Should_Be_Cancellable() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; + + var hasExit = false; + + lifetime.Exit += (_, _) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + lifetime.ShutdownRequested += (_, e) => + { + e.Cancel = true; + ++raised; + }; + + windowA.Close(); + + Assert.False(hasExit); + + windowB.Close(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } + + [Fact] + public void TryShutdown_Cancellable_By_Preventing_Window_Close() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (_, _) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + windowA.Closing += (_, e) => + { + e.Cancel = true; + ++raised; + }; + + lifetime.TryShutdown(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } + + [Fact] + public void Shutdown_NotCancellable_By_Preventing_Window_Close() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (_, _) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + windowA.Closing += (_, e) => + { + e.Cancel = true; + ++raised; + }; + + lifetime.Shutdown(); + + Assert.Equal(1, raised); + Assert.True(hasExit); + } + } + + [Fact] + public void Shutdown_Doesnt_Raise_Shutdown_Requested() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (_, _) => hasExit = true; + + var raised = 0; + + lifetime.ShutdownRequested += (_, _) => + { + ++raised; + }; + + lifetime.Shutdown(); + + Assert.Equal(0, raised); + Assert.True(hasExit); + } + } } }