Skip to content

Commit

Permalink
Merge pull request #7400 from AvaloniaUI/fixes/application-shutdown-osx
Browse files Browse the repository at this point in the history
Fix ClassicDesktop Lifetime so that ShutdownRequested event is raised…
  • Loading branch information
maxkatz6 committed Jan 20, 2022
2 parents fe4197e + 245d23e commit 2950c45
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 46 deletions.
3 changes: 2 additions & 1 deletion src/Avalonia.Controls/ApiCompatBaseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not i
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> 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<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' 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<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> 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<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
Expand All @@ -62,4 +63,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: 63
Total Issues: 64
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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<IPlatformLifetimeEventsImpl>();
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// </summary>
public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime
{
/// <summary>
/// Tries to Shutdown the application. <see cref="ShutdownRequested" /> event can be used to cancel the shutdown.
/// </summary>
/// <param name="exitCode">An integer exit code for an application. The default exit code is 0.</param>
bool TryShutdown(int exitCode = 0);

/// <summary>
/// Gets the arguments passed to the
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/>
Expand Down
8 changes: 6 additions & 2 deletions src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,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();
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
}
}

0 comments on commit 2950c45

Please sign in to comment.