Skip to content

Commit

Permalink
Merge pull request #6174 from AvaloniaUI/feature/2736-applicationShou…
Browse files Browse the repository at this point in the history
…ldTerminate

OSX: Handle applicationShouldTerminate
  • Loading branch information
danwalmsley authored Jul 26, 2021
2 parents e73a6bd + 20098b6 commit 0dea063
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 17 deletions.
6 changes: 6 additions & 0 deletions native/Avalonia.Native/src/OSX/app.mm
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ - (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)ur

_events->FilesOpened(array);
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
return _events->TryShutdown() ? NSTerminateNow : NSTerminateCancel;
}

@end

@interface AvnApplication : NSApplication
Expand Down
13 changes: 1 addition & 12 deletions native/Avalonia.Native/src/OSX/window.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2031,18 +2031,7 @@ -(double) getExtendedTitleBarHeight

+(void)closeAll
{
NSArray<NSWindow*>* 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
Expand Down
5 changes: 4 additions & 1 deletion src/Avalonia.Controls/ApiCompatBaseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownV
MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<System.ComponentModel.CancelEventArgs> 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<System.ComponentModel.CancelEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<System.ComponentModel.CancelEventArgs>)' 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.
Expand All @@ -38,4 +41,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalo
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: 39
Total Issues: 42
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using Avalonia.Controls;
Expand Down Expand Up @@ -42,9 +43,13 @@ public ClassicDesktopStyleApplicationLifetime()
"Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed");
_activeLifetime = this;
}

/// <inheritdoc/>
public event EventHandler<ControlledApplicationLifetimeStartupEventArgs> Startup;

/// <inheritdoc/>
public event EventHandler<CancelEventArgs> ShutdownRequested;

/// <inheritdoc/>
public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit;

Expand Down Expand Up @@ -111,6 +116,11 @@ public int Start(string[] args)
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
}

var lifetimeEvents = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>();

if (lifetimeEvents != null)
lifetimeEvents.ShutdownRequested += OnShutdownRequested;

_cts = new CancellationTokenSource();
MainWindow?.Show();
Dispatcher.UIThread.MainLoop(_cts.Token);
Expand All @@ -123,6 +133,23 @@ public void Dispose()
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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace Avalonia.Controls.ApplicationLifetimes
{
Expand Down Expand Up @@ -34,5 +35,16 @@ public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicatio
Window MainWindow { get; set; }

IReadOnlyList<Window> Windows { get; }

/// <summary>
/// Raised by the platform when a shutdown is requested.
/// </summary>
/// <remarks>
/// 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 <see cref="Window.Closing"/> event on each and allowing
/// each window to cancel the shutdown.
/// </remarks>
event EventHandler<CancelEventArgs> ShutdownRequested;
}
}
16 changes: 16 additions & 0 deletions src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.ComponentModel;

namespace Avalonia.Platform
{
public interface IPlatformLifetimeEventsImpl
{
/// <summary>
/// Raised by the platform when a shutdown is requested.
/// </summary>
/// <remarks>
/// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit.
/// </remarks>
event EventHandler<CancelEventArgs> ShutdownRequested;
}
}
13 changes: 12 additions & 1 deletion src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
Original file line number Diff line number Diff line change
@@ -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<CancelEventArgs> 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();
}
}
}
3 changes: 2 additions & 1 deletion src/Avalonia.Native/AvaloniaNativePlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ void DoInitialize(AvaloniaNativePlatformOptions options)
.Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory));
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(applicationPlatform);

if (_options.UseGpu)
{
Expand Down
1 change: 1 addition & 0 deletions src/Avalonia.Native/avn.idl
Original file line number Diff line number Diff line change
Expand Up @@ -733,4 +733,5 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown
interface IAvnApplicationEvents : IUnknown
{
void FilesOpened (IAvnStringArray* urls);
bool TryShutdown();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Threading;
Expand Down Expand Up @@ -209,6 +210,33 @@ public void Impl_Closing_Should_Remove_Window_From_OpenWindows()
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<IPlatformLifetimeEventsImpl>();
AvaloniaLocator.CurrentMutable.Bind<IPlatformLifetimeEventsImpl>().ToConstant(lifetimeEvents.Object);
lifetime.Start(Array.Empty<string>());

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);
}
}
}

}

0 comments on commit 0dea063

Please sign in to comment.