From 9933f22391b772252b46da608bbfa08a7d4f7f70 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 4 Jan 2024 10:29:42 +0000 Subject: [PATCH 01/11] Add an event so that users can detect when an Application icon is clicked. --- native/Avalonia.Native/src/OSX/app.mm | 6 ++++++ src/Avalonia.Controls/Application.cs | 10 ++++++++++ .../Platform/IApplicationPlatformEvents.cs | 2 ++ .../AvaloniaNativeApplicationPlatform.cs | 5 +++++ src/Avalonia.Native/avn.idl | 1 + 5 files changed, 24 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 88cdf4d9dee..c10370af9a8 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -43,6 +43,12 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; } +-(BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag +{ + _events->Clicked(); + return YES; +} + - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames { auto array = CreateAvnStringArray(filenames); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 44de1c50ebf..3a284166c56 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -63,6 +63,11 @@ public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemp /// public event EventHandler? UrlsOpened; + /// + /// Raised when the application icon is clicked. (MacOS) + /// + public event EventHandler? Clicked; + /// public event EventHandler? ActualThemeVariantChanged; @@ -274,6 +279,11 @@ void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls) UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls)); } + void IApplicationPlatformEvents.Clicked() + { + Clicked?.Invoke(this, EventArgs.Empty); + } + private void NotifyResourcesChanged(ResourcesChangedEventArgs e) { if (_notifyingResourcesChanged) diff --git a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs index 99bbb8b56d0..8b987098c07 100644 --- a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs +++ b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs @@ -6,5 +6,7 @@ namespace Avalonia.Platform public interface IApplicationPlatformEvents { void RaiseUrlsOpened(string[] urls); + + void Clicked(); } } diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index ae24d06badb..a88c5b45492 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -15,6 +15,11 @@ void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray()); } + void IAvnApplicationEvents.Clicked() + { + ((IApplicationPlatformEvents)Application.Current).Clicked(); + } + public int TryShutdown() { if (ShutdownRequested is null) return 1; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 7583dfdef65..3ce5206c983 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1087,6 +1087,7 @@ interface IAvnApplicationEvents : IUnknown { void FilesOpened (IAvnStringArray* urls); bool TryShutdown(); + void Clicked (); } [uuid(b4284791-055b-4313-8c2e-50f0a8c72ce9)] From bca24ff7b35fab6776efc7264cdf555a7813a454 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Jan 2024 11:28:57 +0000 Subject: [PATCH 02/11] refactor to use Lifetime apis. --- native/Avalonia.Native/src/OSX/app.mm | 12 +++++++++- src/Avalonia.Controls/Application.cs | 10 -------- .../ActivatedEventArgs.cs | 13 ++++++++++ .../ApplicationLifetimes/ActivationReason.cs | 9 +++++++ .../ClassicDesktopStyleApplicationLifetime.cs | 13 ++++++---- .../IActivatableApplicationLifetime.cs | 10 ++++++++ .../Platform/IApplicationPlatformEvents.cs | 2 -- .../AvaloniaNativeApplicationPlatform.cs | 23 ++++++++++++++++-- .../AvaloniaNativePlatformExtensions.cs | 4 ++++ ...SClassicDesktopStyleApplicationLifetime.cs | 24 +++++++++++++++++++ src/Avalonia.Native/avn.idl | 4 +++- 11 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/ActivationReason.cs create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs create mode 100644 src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index c10370af9a8..3a0a60a19c3 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -45,10 +45,20 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification -(BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag { - _events->Clicked(); + _events->OnReopen(); return YES; } +- (void)applicationDidHide:(NSNotification *)notification +{ + _events->OnHide(); +} + +- (void)applicationDidUnhide:(NSNotification *)notification +{ + _events->OnUnhide(); +} + - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames { auto array = CreateAvnStringArray(filenames); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 3a284166c56..44de1c50ebf 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -63,11 +63,6 @@ public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemp /// public event EventHandler? UrlsOpened; - /// - /// Raised when the application icon is clicked. (MacOS) - /// - public event EventHandler? Clicked; - /// public event EventHandler? ActualThemeVariantChanged; @@ -279,11 +274,6 @@ void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls) UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls)); } - void IApplicationPlatformEvents.Clicked() - { - Clicked?.Invoke(this, EventArgs.Empty); - } - private void NotifyResourcesChanged(ResourcesChangedEventArgs e) { if (_notifyingResourcesChanged) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs new file mode 100644 index 00000000000..e21d83ef9a9 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace Avalonia.Controls.ApplicationLifetimes; + +public class ActivatedEventArgs : EventArgs +{ + public ActivatedEventArgs(ActivationReason reason) + { + Reason = reason; + } + + public ActivationReason Reason { get; } +} diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivationReason.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivationReason.cs new file mode 100644 index 00000000000..9fc1588c9d1 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivationReason.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Controls.ApplicationLifetimes; + +public enum ActivationReason +{ + Launched = 10, + OpenUri = 20, + Reopen = 30, // i.e. Dock Icon clicked on MacOS + Background = 40 // i.e. Entered or left background. +} diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 77f3b93efa1..f235d0e2cf5 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -211,11 +211,16 @@ public static class ClassicDesktopStyleApplicationLifetimeExtensions public static int StartWithClassicDesktopLifetime( this AppBuilder builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) { - var lifetime = new ClassicDesktopStyleApplicationLifetime() + var lifetime = AvaloniaLocator.Current.GetService(); + + if (lifetime == null) { - Args = args, - ShutdownMode = shutdownMode - }; + lifetime = new ClassicDesktopStyleApplicationLifetime(); + } + + lifetime.Args = args; + lifetime.ShutdownMode = shutdownMode; + builder.SetupWithLifetime(lifetime); return lifetime.Start(args); } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs new file mode 100644 index 00000000000..b37c9fd6844 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs @@ -0,0 +1,10 @@ +using System; + +namespace Avalonia.Controls.ApplicationLifetimes; + +public interface IActivatableApplicationLifetime +{ + event EventHandler Activated; + + event EventHandler Deactivated; +} diff --git a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs index 8b987098c07..99bbb8b56d0 100644 --- a/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs +++ b/src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs @@ -6,7 +6,5 @@ namespace Avalonia.Platform public interface IApplicationPlatformEvents { void RaiseUrlsOpened(string[] urls); - - void Clicked(); } } diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index a88c5b45492..4f04f508b4a 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -15,9 +15,28 @@ void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray()); } - void IAvnApplicationEvents.Clicked() + void IAvnApplicationEvents.OnReopen() { - ((IApplicationPlatformEvents)Application.Current).Clicked(); + if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.RaiseActivated(ActivationReason.Reopen); + } + } + + void IAvnApplicationEvents.OnHide() + { + if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.RaiseDeactivated(ActivationReason.Background); + } + } + + void IAvnApplicationEvents.OnUnhide() + { + if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.RaiseActivated(ActivationReason.Background); + } } public int TryShutdown() diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 1d271c9d759..ccd358ff8e2 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Native; namespace Avalonia @@ -24,6 +25,9 @@ public static AppBuilder UseAvaloniaNative(this AppBuilder builder) }); }); + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(new MacOSClassicDesktopStyleApplicationLifetime()); + return builder; } } diff --git a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs new file mode 100644 index 00000000000..d5037cff156 --- /dev/null +++ b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Controls.ApplicationLifetimes; + +namespace Avalonia.Native; + +#nullable enable + +internal class MacOSClassicDesktopStyleApplicationLifetime : ClassicDesktopStyleApplicationLifetime, + IActivatableApplicationLifetime +{ + public event EventHandler? Activated; + + public event EventHandler? Deactivated; + + internal void RaiseActivated(ActivationReason reason) + { + Activated?.Invoke(this, new ActivatedEventArgs(reason)); + } + + internal void RaiseDeactivated(ActivationReason reason) + { + Deactivated?.Invoke(this, new ActivatedEventArgs(reason)); + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 3ce5206c983..1b305ed7b77 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1087,7 +1087,9 @@ interface IAvnApplicationEvents : IUnknown { void FilesOpened (IAvnStringArray* urls); bool TryShutdown(); - void Clicked (); + void OnReopen (); + void OnHide (); + void OnUnhide (); } [uuid(b4284791-055b-4313-8c2e-50f0a8c72ce9)] From b5fd8ad98cb2a2908358358664caba65cb2af7af Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Jan 2024 11:37:47 +0000 Subject: [PATCH 03/11] use ActivationKind instead of reason to be consistent with other xaml platforms --- .../ApplicationLifetimes/ActivatedEventArgs.cs | 6 +++--- .../{ActivationReason.cs => ActivationKind.cs} | 3 +-- .../ProtocolActivatedEventArgs.cs | 13 +++++++++++++ .../AvaloniaNativeApplicationPlatform.cs | 6 +++--- .../MacOSClassicDesktopStyleApplicationLifetime.cs | 9 ++++++--- 5 files changed, 26 insertions(+), 11 deletions(-) rename src/Avalonia.Controls/ApplicationLifetimes/{ActivationReason.cs => ActivationKind.cs} (79%) create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs index e21d83ef9a9..680c3f01ccc 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs @@ -4,10 +4,10 @@ namespace Avalonia.Controls.ApplicationLifetimes; public class ActivatedEventArgs : EventArgs { - public ActivatedEventArgs(ActivationReason reason) + public ActivatedEventArgs(ActivationKind kind) { - Reason = reason; + Kind = kind; } - public ActivationReason Reason { get; } + public ActivationKind Kind { get; } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivationReason.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs similarity index 79% rename from src/Avalonia.Controls/ApplicationLifetimes/ActivationReason.cs rename to src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs index 9fc1588c9d1..aae786f380e 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ActivationReason.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs @@ -1,8 +1,7 @@ namespace Avalonia.Controls.ApplicationLifetimes; -public enum ActivationReason +public enum ActivationKind { - Launched = 10, OpenUri = 20, Reopen = 30, // i.e. Dock Icon clicked on MacOS Background = 40 // i.e. Entered or left background. diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs new file mode 100644 index 00000000000..e9706f1cf90 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace Avalonia.Controls.ApplicationLifetimes; + +public class ProtocolActivatedEventArgs : ActivatedEventArgs +{ + public ProtocolActivatedEventArgs(ActivationKind kind, Uri uri) : base(kind) + { + Uri = uri; + } + + public Uri Uri { get; } +} diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index 4f04f508b4a..c48c98dc743 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -19,7 +19,7 @@ void IAvnApplicationEvents.OnReopen() { if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) { - lifetime.RaiseActivated(ActivationReason.Reopen); + lifetime.RaiseActivated(ActivationKind.Reopen); } } @@ -27,7 +27,7 @@ void IAvnApplicationEvents.OnHide() { if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) { - lifetime.RaiseDeactivated(ActivationReason.Background); + lifetime.RaiseDeactivated(ActivationKind.Background); } } @@ -35,7 +35,7 @@ void IAvnApplicationEvents.OnUnhide() { if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) { - lifetime.RaiseActivated(ActivationReason.Background); + lifetime.RaiseActivated(ActivationKind.Background); } } diff --git a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs index d5037cff156..6afaa9e36e8 100644 --- a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs @@ -12,13 +12,16 @@ internal class MacOSClassicDesktopStyleApplicationLifetime : ClassicDesktopStyle public event EventHandler? Deactivated; - internal void RaiseActivated(ActivationReason reason) { Activated?.Invoke(this, new ActivatedEventArgs(reason)); + + internal void RaiseActivated(ActivationKind kind) + { + Activated?.Invoke(this, new ActivatedEventArgs(kind)); } - internal void RaiseDeactivated(ActivationReason reason) + internal void RaiseDeactivated(ActivationKind kind) { - Deactivated?.Invoke(this, new ActivatedEventArgs(reason)); + Deactivated?.Invoke(this, new ActivatedEventArgs(kind)); } } From 440f300fa905e2111c7e1126eec7a3132b031eff Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Jan 2024 11:38:07 +0000 Subject: [PATCH 04/11] implement macos raise url events. --- src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs | 8 ++++++++ .../MacOSClassicDesktopStyleApplicationLifetime.cs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index c48c98dc743..3365da7d360 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -13,6 +13,14 @@ internal class AvaloniaNativeApplicationPlatform : NativeCallbackBase, IAvnAppli void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) { ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray()); + + if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) + { + foreach (var url in urls.ToStringArray()) + { + lifetime.RaiseUrl(new Uri(url)); + } + } } void IAvnApplicationEvents.OnReopen() diff --git a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs index 6afaa9e36e8..ba404ea4d6a 100644 --- a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs @@ -12,8 +12,10 @@ internal class MacOSClassicDesktopStyleApplicationLifetime : ClassicDesktopStyle public event EventHandler? Deactivated; + internal void RaiseUrl(Uri uri) { - Activated?.Invoke(this, new ActivatedEventArgs(reason)); + Activated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, uri)); + } internal void RaiseActivated(ActivationKind kind) { From 6dbb7eadbb4171bca954199bc1cf38e1cabf9853 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Jan 2024 13:30:21 +0000 Subject: [PATCH 05/11] add docs. --- .../ActivatedEventArgs.cs | 12 +++++++++- .../ApplicationLifetimes/ActivationKind.cs | 23 ++++++++++++++++--- .../IActivatableApplicationLifetime.cs | 11 +++++++++ ...SClassicDesktopStyleApplicationLifetime.cs | 2 ++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs index 680c3f01ccc..fb2eae1b529 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs @@ -2,12 +2,22 @@ namespace Avalonia.Controls.ApplicationLifetimes; +/// +/// Event args for an Application Lifetime Activated or Deactivated events. +/// public class ActivatedEventArgs : EventArgs { + /// + /// Ctor for ActivatedEventArgs + /// + /// The that this event represents public ActivatedEventArgs(ActivationKind kind) { Kind = kind; } - + + /// + /// The that this event represents. + /// public ActivationKind Kind { get; } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs index aae786f380e..6d72b56921b 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs @@ -2,7 +2,24 @@ namespace Avalonia.Controls.ApplicationLifetimes; public enum ActivationKind { - OpenUri = 20, - Reopen = 30, // i.e. Dock Icon clicked on MacOS - Background = 40 // i.e. Entered or left background. + /// + /// When the application is passed a URI to open. + /// + OpenUri = 20, + + /// + /// When the application is asked to reopen. + /// An example of this is on MacOS when all the windows are closed, + /// application continues to run in the background and the user clicks + /// the application's dock icon. + /// + Reopen = 30, + + /// + /// When the application enters or leaves a background state. + /// An example is when on MacOS the user hides or shows and application (not window), + /// or when a browser application switchs tabs or when a mobile applications goes into + /// the background. + /// + Background = 40 } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs index b37c9fd6844..64b0215bd71 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs @@ -2,9 +2,20 @@ namespace Avalonia.Controls.ApplicationLifetimes; +/// +/// An interface for ApplicationLifetimes where the application can be Activated and Deactivated. +/// public interface IActivatableApplicationLifetime { + /// + /// An event that is raised when the application is Activated for various reasons + /// as described by the enumeration. + /// event EventHandler Activated; + /// + /// An event that is raised when the application is Deactivated for various reasons + /// as described by the enumeration. + /// event EventHandler Deactivated; } diff --git a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs index ba404ea4d6a..0c72623963f 100644 --- a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs @@ -8,8 +8,10 @@ namespace Avalonia.Native; internal class MacOSClassicDesktopStyleApplicationLifetime : ClassicDesktopStyleApplicationLifetime, IActivatableApplicationLifetime { + /// public event EventHandler? Activated; + /// public event EventHandler? Deactivated; internal void RaiseUrl(Uri uri) From 3366aa9a27c23cedd89dcdab59ffa840099d955d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 8 Jan 2024 15:45:22 +0000 Subject: [PATCH 06/11] add apis to programatically Activate and Deactivate the application. This allows the dock icon to be kept in sync so its menu options there say "Hide" / "Show" correctly. --- native/Avalonia.Native/src/OSX/app.mm | 7 +++++++ native/Avalonia.Native/src/OSX/common.h | 1 + .../IActivatableApplicationLifetime.cs | 12 ++++++++++++ .../Platform/INativeApplicationCommands.cs | 1 + ...MacOSClassicDesktopStyleApplicationLifetime.cs | 15 +++++++++++++++ src/Avalonia.Native/MacOSNativeMenuCommands.cs | 6 +++++- src/Avalonia.Native/avn.idl | 1 + 7 files changed, 42 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 3a0a60a19c3..7630de1d0bc 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -139,6 +139,13 @@ extern void ReleaseAvnAppEvents() } } +HRESULT AvnApplicationCommands::UnhideApp() +{ + START_COM_CALL; + [[NSApplication sharedApplication] unhide:[NSApp delegate]]; + return S_OK; +} + HRESULT AvnApplicationCommands::HideApp() { START_COM_CALL; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 44441ee15db..a3473d39281 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -101,6 +101,7 @@ class AvnApplicationCommands : public ComSingleObject enumeration. /// event EventHandler Deactivated; + + /// + /// Tells the platform (OS) to activate the application. + /// For example on OSX this would be [NSApp unhide] + /// + public void Activate(); + + /// + /// Tells the platform (OS) to deactivate the application. + /// For example on OSX this would be [NSApp hide]. + /// + public void Deactivate(); } diff --git a/src/Avalonia.Controls/Platform/INativeApplicationCommands.cs b/src/Avalonia.Controls/Platform/INativeApplicationCommands.cs index d837e4155e9..d287bf98b5d 100644 --- a/src/Avalonia.Controls/Platform/INativeApplicationCommands.cs +++ b/src/Avalonia.Controls/Platform/INativeApplicationCommands.cs @@ -5,6 +5,7 @@ namespace Avalonia.Controls.Platform /// internal interface INativeApplicationCommands { + void ShowApp(); void HideApp(); void ShowAll(); void HideOthers(); diff --git a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs index 0c72623963f..b6aa5289f13 100644 --- a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Platform; namespace Avalonia.Native; @@ -14,6 +15,20 @@ internal class MacOSClassicDesktopStyleApplicationLifetime : ClassicDesktopStyle /// public event EventHandler? Deactivated; + /// + public void Activate() + { + var nativeApplicationCommands = AvaloniaLocator.Current.GetService(); + nativeApplicationCommands?.ShowApp(); + } + + /// + public void Deactivate() + { + var nativeApplicationCommands = AvaloniaLocator.Current.GetService(); + nativeApplicationCommands?.HideApp(); + } + internal void RaiseUrl(Uri uri) { Activated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, uri)); diff --git a/src/Avalonia.Native/MacOSNativeMenuCommands.cs b/src/Avalonia.Native/MacOSNativeMenuCommands.cs index 6d467d307b4..0baca79264f 100644 --- a/src/Avalonia.Native/MacOSNativeMenuCommands.cs +++ b/src/Avalonia.Native/MacOSNativeMenuCommands.cs @@ -13,6 +13,11 @@ public MacOSNativeMenuCommands(IAvnApplicationCommands commands) _commands = commands; } + public void ShowApp() + { + _commands.UnhideApp(); + } + public void HideApp() { _commands.HideApp(); @@ -28,7 +33,6 @@ public void HideOthers() _commands.HideOthers(); } - public static readonly AttachedProperty IsServicesSubmenuProperty = AvaloniaProperty.RegisterAttached("IsServicesSubmenu", false); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 1b305ed7b77..7791426c0b1 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1095,6 +1095,7 @@ interface IAvnApplicationEvents : IUnknown [uuid(b4284791-055b-4313-8c2e-50f0a8c72ce9)] interface IAvnApplicationCommands : IUnknown { + HRESULT UnhideApp(); HRESULT HideApp(); HRESULT ShowAll(); HRESULT HideOthers(); From 7035aba750c1a35c19158b2ba7266836bff3be84 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 9 Jan 2024 08:51:07 +0000 Subject: [PATCH 07/11] fix api naming. --- .../IActivatableApplicationLifetime.cs | 12 +++++++----- .../MacOSClassicDesktopStyleApplicationLifetime.cs | 8 ++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs index 12ca5601ab8..fbdfe3aa7d7 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs @@ -20,14 +20,16 @@ public interface IActivatableApplicationLifetime event EventHandler Deactivated; /// - /// Tells the platform (OS) to activate the application. + /// Tells the application that it should attempt to leave its background state. /// For example on OSX this would be [NSApp unhide] /// - public void Activate(); + /// true if it was possible and the platform supports this. false otherwise + public bool TryLeaveBackground(); /// - /// Tells the platform (OS) to deactivate the application. - /// For example on OSX this would be [NSApp hide]. + /// Tells the application that it should attempt to enter its background state. + /// For example on OSX this would be [NSApp hide] /// - public void Deactivate(); + /// true if it was possible and the platform supports this. false otherwise + public bool TryEnterBackground(); } diff --git a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs index b6aa5289f13..caf6e03ec8f 100644 --- a/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs @@ -16,17 +16,21 @@ internal class MacOSClassicDesktopStyleApplicationLifetime : ClassicDesktopStyle public event EventHandler? Deactivated; /// - public void Activate() + public bool TryLeaveBackground() { var nativeApplicationCommands = AvaloniaLocator.Current.GetService(); nativeApplicationCommands?.ShowApp(); + + return true; } /// - public void Deactivate() + public bool TryEnterBackground() { var nativeApplicationCommands = AvaloniaLocator.Current.GetService(); nativeApplicationCommands?.HideApp(); + + return true; } internal void RaiseUrl(Uri uri) From 71ec00c512e9d9379678cf3ed5b72e6a53112b4b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 9 Jan 2024 20:02:28 -0800 Subject: [PATCH 08/11] Add Browser IActivatableApplicationLifetime impl --- samples/ControlCatalog/App.xaml.cs | 8 +++++ .../Avalonia.Browser/BrowserAppBuilder.cs | 3 +- .../BrowserSingleViewLifetime.cs | 31 ++++++++++++++++++- .../Avalonia.Browser/Interop/InputHelper.cs | 5 ++- .../webapp/modules/avalonia/input.ts | 8 +++++ 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 605254c995f..6a941c21aef 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -51,6 +51,14 @@ public override void OnFrameworkInitializationCompleted() singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() }; } + if (ApplicationLifetime is IActivatableApplicationLifetime activatableApplicationLifetime) + { + activatableApplicationLifetime.Activated += (sender, args) => + Console.WriteLine($"App activated: {args.Kind}"); + activatableApplicationLifetime.Deactivated += (sender, args) => + Console.WriteLine($"App deactivated: {args.Kind}"); + } + base.OnFrameworkInitializationCompleted(); } diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index 996faaec58e..eaa72c19312 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -75,8 +75,9 @@ public static async Task SetupBrowserAppAsync(this AppBuilder builder, BrowserPl { builder = await PreSetupBrowser(builder, options); + var lifetime = new BrowserSingleViewLifetime(); builder - .SetupWithoutStarting(); + .SetupWithLifetime(lifetime); } internal static async Task PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options) diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index 6fa79f6f54d..6607bcf5395 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -4,11 +4,34 @@ using Avalonia.Controls.ApplicationLifetimes; using System.Runtime.Versioning; using Avalonia.Browser; +using Avalonia.Browser.Interop; +using Avalonia.Threading; namespace Avalonia; -internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime +internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime, IActivatableApplicationLifetime { + public BrowserSingleViewLifetime() + { + bool? initiallyVisible = InputHelper.SubscribeVisibilityChange(visible => + { + initiallyVisible = null; + (visible ? Activated : Deactivated)?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); + }); + + // Trigger Activated as an initial state, if web page is visible, and wasn't hidden during initialization. + if (initiallyVisible == true) + { + Dispatcher.UIThread.Invoke(() => + { + if (initiallyVisible == true) + { + Activated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); + } + }); + } + } + public AvaloniaView? View; public Control? MainView @@ -33,4 +56,10 @@ private void EnsureView() throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called."); } } + + public event EventHandler? Activated; + public event EventHandler? Deactivated; + + public bool TryLeaveBackground() => false; + public bool TryEnterBackground() => true; } diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index 9fc0df28d87..7f294e155ec 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -50,11 +50,14 @@ public static partial void SubscribeInputEvents( [JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeDropEvents(JSObject containerElement, [JSMarshalAs>] Func dragEvent); - + [JSImport("InputHelper.subscribeKeyboardGeometryChange", AvaloniaModule.MainModuleName)] public static partial void SubscribeKeyboardGeometryChange(JSObject containerElement, [JSMarshalAs>] Func handler); + [JSImport("InputHelper.subscribeVisibilityChange", AvaloniaModule.MainModuleName)] + public static partial bool SubscribeVisibilityChange([JSMarshalAs>] Action handler); + [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)] [return: JSMarshalAs>] public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index 99439b6034a..e03aa9e5323 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -263,6 +263,14 @@ export class InputHelper { } } + public static subscribeVisibilityChange( + handler: (state: boolean) => void): boolean { + document.addEventListener("visibilitychange", () => { + handler(document.visibilityState === "visible"); + }); + return document.visibilityState === "visible"; + } + public static clearInput(inputElement: HTMLInputElement) { inputElement.value = ""; } From 9c0c193b5ab9ad9644bcc4a506dd398cb76d3f13 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 9 Jan 2024 21:58:10 -0800 Subject: [PATCH 09/11] Implement IActivatableApplicationLifetime on Android --- .../ControlCatalog.Android/MainActivity.cs | 7 +++- .../AvaloniaMainActivity.App.cs | 2 +- .../Avalonia.Android/AvaloniaMainActivity.cs | 40 ++++++++++++++++++- .../IAndroidNavigationService.cs | 7 ++++ .../Avalonia.Android/SingleViewLifetime.cs | 23 ++++++++++- 5 files changed, 73 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 9b5b06e5fb3..fce568b15e7 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -2,13 +2,16 @@ using Android.Content.PM; using Avalonia; using Avalonia.Android; +using static Android.Content.Intent; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] + // IntentFilter are here to test IActivatableApplicationLifetime + [IntentFilter(new [] { ActionView }, Categories = new [] { CategoryDefault, CategoryBrowsable }, DataScheme = "avln" )] public class MainActivity : AvaloniaMainActivity { - protected override Avalonia.AppBuilder CustomizeAppBuilder(Avalonia.AppBuilder builder) + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { return base.CustomizeAppBuilder(builder) .AfterSetup(_ => diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs index 3a32c269d7f..cf258a5a1f4 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs @@ -36,7 +36,7 @@ private void InitializeApp() { var builder = CreateAppBuilder(); - builder.SetupWithLifetime(new SingleViewLifetime()); + builder.SetupWithLifetime(new SingleViewLifetime(this)); s_appBuilder = builder; } diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index a4175f864f6..d9aa2e95a14 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -7,15 +7,29 @@ using Android.Runtime; using Android.Views; using AndroidX.AppCompat.App; +using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Android { - public class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService + public class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService, IActivableActivity { + private EventHandler _onActivated, _onDeactivated; + public Action ActivityResult { get; set; } public Action RequestPermissionsResult { get; set; } public event EventHandler BackRequested; + event EventHandler IActivableActivity.Activated + { + add { _onActivated += value; } + remove { _onActivated -= value; } + } + + event EventHandler IActivableActivity.Deactivated + { + add { _onDeactivated += value; } + remove { _onDeactivated -= value; } + } public override void OnBackPressed() { @@ -29,6 +43,30 @@ public override void OnBackPressed() } } + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + + if (Intent?.Data is {} androidUri + && androidUri.IsAbsolute + && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri)) + { + _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri)); + } + } + + protected override void OnStop() + { + _onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); + base.OnStop(); + } + + protected override void OnStart() + { + _onActivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); + base.OnStart(); + } + protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) { base.OnActivityResult(requestCode, resultCode, data); diff --git a/src/Android/Avalonia.Android/IAndroidNavigationService.cs b/src/Android/Avalonia.Android/IAndroidNavigationService.cs index 5fa93971d96..f5f46388ef4 100644 --- a/src/Android/Avalonia.Android/IAndroidNavigationService.cs +++ b/src/Android/Avalonia.Android/IAndroidNavigationService.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Android { @@ -7,6 +8,12 @@ public interface IActivityNavigationService event EventHandler BackRequested; } + public interface IActivableActivity + { + event EventHandler Activated; + event EventHandler Deactivated; + } + public class AndroidBackRequestedEventArgs : EventArgs { public bool Handled { get; set; } diff --git a/src/Android/Avalonia.Android/SingleViewLifetime.cs b/src/Android/Avalonia.Android/SingleViewLifetime.cs index eef763a932b..e20a101920c 100644 --- a/src/Android/Avalonia.Android/SingleViewLifetime.cs +++ b/src/Android/Avalonia.Android/SingleViewLifetime.cs @@ -1,12 +1,26 @@ -using Avalonia.Controls; +using System; +using Android.App; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Android { - internal class SingleViewLifetime : ISingleViewApplicationLifetime + internal class SingleViewLifetime : ISingleViewApplicationLifetime, IActivatableApplicationLifetime { + private readonly Activity _activity; private AvaloniaView _view; + public SingleViewLifetime(Activity activity) + { + _activity = activity; + + if (activity is IActivableActivity activableActivity) + { + activableActivity.Activated += (_, args) => Activated?.Invoke(this, args); + activableActivity.Deactivated += (_, args) => Deactivated?.Invoke(this, args); + } + } + public AvaloniaView View { get => _view; internal set @@ -22,5 +36,10 @@ public AvaloniaView View } public Control MainView { get; set; } + public event EventHandler Activated; + public event EventHandler Deactivated; + + public bool TryLeaveBackground() => _activity.MoveTaskToBack(true); + public bool TryEnterBackground() => false; } } From a20498ae7c3216e5534be26c8ba3994e8642d449 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 9 Jan 2024 22:28:31 -0800 Subject: [PATCH 10/11] Add IActivatableApplicationLifetime iOS implementation --- src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 58 +++++++++++++++++---- src/iOS/Avalonia.iOS/SingleViewLifetime.cs | 27 ++++++++++ 2 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 src/iOS/Avalonia.iOS/SingleViewLifetime.cs diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index c1e7cd5aba4..ecb9e56aa94 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -1,25 +1,39 @@ +using System; using Foundation; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using UIKit; namespace Avalonia.iOS { - public class AvaloniaAppDelegate : UIResponder, IUIApplicationDelegate + public interface IAvaloniaAppDelegate + { + event EventHandler Activated; + event EventHandler Deactivated; + } + + public class AvaloniaAppDelegate : UIResponder, IUIApplicationDelegate, IAvaloniaAppDelegate where TApp : Application, new() { - class SingleViewLifetime : ISingleViewApplicationLifetime - { - public AvaloniaView View; + private EventHandler _onActivated, _onDeactivated; - public Control MainView - { - get => View.Content; - set => View.Content = value; - } + public AvaloniaAppDelegate() + { + NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.DidEnterBackgroundNotification, OnEnteredBackground); + NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.WillEnterForegroundNotification, OnLeavingBackground); } + event EventHandler IAvaloniaAppDelegate.Activated + { + add { _onActivated += value; } + remove { _onActivated -= value; } + } + event EventHandler IAvaloniaAppDelegate.Deactivated + { + add { _onDeactivated += value; } + remove { _onDeactivated -= value; } + } + protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; [Export("window")] @@ -30,7 +44,7 @@ public bool FinishedLaunching(UIApplication application, NSDictionary launchOpti { var builder = AppBuilder.Configure().UseiOS(); - var lifetime = new SingleViewLifetime(); + var lifetime = new SingleViewLifetime(this); builder.AfterSetup(_ => { @@ -54,5 +68,27 @@ public bool FinishedLaunching(UIApplication application, NSDictionary launchOpti return true; } + + [Export("application:openURL:options:")] + public bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options) + { + if (Uri.TryCreate(url.ToString(), UriKind.Absolute, out var uri)) + { + _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, uri)); + return true; + } + + return false; + } + + private void OnEnteredBackground(NSNotification notification) + { + _onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); + } + + private void OnLeavingBackground(NSNotification notification) + { + _onActivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); + } } } diff --git a/src/iOS/Avalonia.iOS/SingleViewLifetime.cs b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs new file mode 100644 index 00000000000..d3924482e42 --- /dev/null +++ b/src/iOS/Avalonia.iOS/SingleViewLifetime.cs @@ -0,0 +1,27 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; + +namespace Avalonia.iOS; + +internal class SingleViewLifetime : ISingleViewApplicationLifetime, IActivatableApplicationLifetime +{ + public SingleViewLifetime(IAvaloniaAppDelegate avaloniaAppDelegate) + { + avaloniaAppDelegate.Activated += (_, args) => Activated?.Invoke(this, args); + avaloniaAppDelegate.Deactivated += (_, args) => Deactivated?.Invoke(this, args); + } + + public AvaloniaView View; + + public Control MainView + { + get => View.Content; + set => View.Content = value; + } + + public event EventHandler Activated; + public event EventHandler Deactivated; + public bool TryLeaveBackground() => false; + public bool TryEnterBackground() => false; +} From 0ea340cb268b84d50258df7a69d48a164fe9e415 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 9 Jan 2024 22:30:46 -0800 Subject: [PATCH 11/11] Adjust android impl a little --- src/Android/Avalonia.Android/AvaloniaMainActivity.cs | 6 +++--- .../Avalonia.Android/IAndroidNavigationService.cs | 6 ------ src/Android/Avalonia.Android/IAvaloniaActivity.cs | 10 ++++++++++ src/Android/Avalonia.Android/SingleViewLifetime.cs | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 src/Android/Avalonia.Android/IAvaloniaActivity.cs diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index d9aa2e95a14..b8b39066132 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -11,7 +11,7 @@ namespace Avalonia.Android { - public class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, IActivityNavigationService, IActivableActivity + public class AvaloniaMainActivity : AppCompatActivity, IAvaloniaActivity { private EventHandler _onActivated, _onDeactivated; @@ -19,13 +19,13 @@ public class AvaloniaMainActivity : AppCompatActivity, IActivityResultHandler, I public Action RequestPermissionsResult { get; set; } public event EventHandler BackRequested; - event EventHandler IActivableActivity.Activated + event EventHandler IAvaloniaActivity.Activated { add { _onActivated += value; } remove { _onActivated -= value; } } - event EventHandler IActivableActivity.Deactivated + event EventHandler IAvaloniaActivity.Deactivated { add { _onDeactivated += value; } remove { _onDeactivated -= value; } diff --git a/src/Android/Avalonia.Android/IAndroidNavigationService.cs b/src/Android/Avalonia.Android/IAndroidNavigationService.cs index f5f46388ef4..b45ee6e78db 100644 --- a/src/Android/Avalonia.Android/IAndroidNavigationService.cs +++ b/src/Android/Avalonia.Android/IAndroidNavigationService.cs @@ -8,12 +8,6 @@ public interface IActivityNavigationService event EventHandler BackRequested; } - public interface IActivableActivity - { - event EventHandler Activated; - event EventHandler Deactivated; - } - public class AndroidBackRequestedEventArgs : EventArgs { public bool Handled { get; set; } diff --git a/src/Android/Avalonia.Android/IAvaloniaActivity.cs b/src/Android/Avalonia.Android/IAvaloniaActivity.cs new file mode 100644 index 00000000000..005096a0bd8 --- /dev/null +++ b/src/Android/Avalonia.Android/IAvaloniaActivity.cs @@ -0,0 +1,10 @@ +using System; +using Avalonia.Controls.ApplicationLifetimes; + +namespace Avalonia.Android; + +public interface IAvaloniaActivity : IActivityResultHandler, IActivityNavigationService +{ + event EventHandler Activated; + event EventHandler Deactivated; +} diff --git a/src/Android/Avalonia.Android/SingleViewLifetime.cs b/src/Android/Avalonia.Android/SingleViewLifetime.cs index e20a101920c..f8a2ee28947 100644 --- a/src/Android/Avalonia.Android/SingleViewLifetime.cs +++ b/src/Android/Avalonia.Android/SingleViewLifetime.cs @@ -14,7 +14,7 @@ public SingleViewLifetime(Activity activity) { _activity = activity; - if (activity is IActivableActivity activableActivity) + if (activity is IAvaloniaActivity activableActivity) { activableActivity.Activated += (_, args) => Activated?.Invoke(this, args); activableActivity.Deactivated += (_, args) => Deactivated?.Invoke(this, args);