From 8bd49413f77af5d188cbce9c898e5fcb83863cf7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 10 Jan 2024 08:06:07 +0000 Subject: [PATCH] Add an event so that users can detect when an Application icon is clicked (#14106) * Add an event so that users can detect when an Application icon is clicked. * refactor to use Lifetime apis. * use ActivationKind instead of reason to be consistent with other xaml platforms * implement macos raise url events. * add docs. * 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. * fix api naming. * Add Browser IActivatableApplicationLifetime impl * Implement IActivatableApplicationLifetime on Android * Add IActivatableApplicationLifetime iOS implementation * Adjust android impl a little --------- Co-authored-by: Max Katz #Conflicts: # src/Browser/Avalonia.Browser/Interop/InputHelper.cs # src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts --- native/Avalonia.Native/src/OSX/app.mm | 23 ++++++++ native/Avalonia.Native/src/OSX/common.h | 1 + .../ControlCatalog.Android/MainActivity.cs | 7 ++- samples/ControlCatalog/App.xaml.cs | 8 +++ .../AvaloniaMainActivity.App.cs | 2 +- .../Avalonia.Android/AvaloniaMainActivity.cs | 40 ++++++++++++- .../IAndroidNavigationService.cs | 1 + .../Avalonia.Android/IAvaloniaActivity.cs | 10 ++++ .../Avalonia.Android/SingleViewLifetime.cs | 23 +++++++- .../ActivatedEventArgs.cs | 23 ++++++++ .../ApplicationLifetimes/ActivationKind.cs | 25 ++++++++ .../ClassicDesktopStyleApplicationLifetime.cs | 13 +++-- .../IActivatableApplicationLifetime.cs | 35 +++++++++++ .../ProtocolActivatedEventArgs.cs | 13 +++++ .../Platform/INativeApplicationCommands.cs | 1 + .../AvaloniaNativeApplicationPlatform.cs | 32 ++++++++++ .../AvaloniaNativePlatformExtensions.cs | 4 ++ ...SClassicDesktopStyleApplicationLifetime.cs | 50 ++++++++++++++++ .../MacOSNativeMenuCommands.cs | 6 +- src/Avalonia.Native/avn.idl | 4 ++ .../Avalonia.Browser/BrowserAppBuilder.cs | 3 +- .../BrowserSingleViewLifetime.cs | 19 +++++- .../Avalonia.Browser/Interop/InputHelper.cs | 9 ++- .../webapp/modules/avalonia/input.ts | 26 +++++++++ src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 58 +++++++++++++++---- src/iOS/Avalonia.iOS/SingleViewLifetime.cs | 27 +++++++++ 26 files changed, 438 insertions(+), 25 deletions(-) create mode 100644 src/Android/Avalonia.Android/IAvaloniaActivity.cs create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs create mode 100644 src/Avalonia.Controls/ApplicationLifetimes/ProtocolActivatedEventArgs.cs create mode 100644 src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs create mode 100644 src/iOS/Avalonia.iOS/SingleViewLifetime.cs diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 88cdf4d9dee..7630de1d0bc 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -43,6 +43,22 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; } +-(BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag +{ + _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); @@ -123,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 672525c64a8..2d23b2e850e 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -100,6 +100,7 @@ class AvnApplicationCommands : public ComSingleObject { - protected override Avalonia.AppBuilder CustomizeAppBuilder(Avalonia.AppBuilder builder) + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) { return base.CustomizeAppBuilder(builder) .AfterSetup(_ => 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/Android/Avalonia.Android/AvaloniaMainActivity.App.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs index 3fcfde3ee43..80e8738805a 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs @@ -40,7 +40,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 499924d8fb8..70e8ce5983a 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -8,15 +8,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, IAvaloniaActivity { + private EventHandler _onActivated, _onDeactivated; + public Action ActivityResult { get; set; } public Action RequestPermissionsResult { get; set; } public event EventHandler BackRequested; + event EventHandler IAvaloniaActivity.Activated + { + add { _onActivated += value; } + remove { _onActivated -= value; } + } + + event EventHandler IAvaloniaActivity.Deactivated + { + add { _onDeactivated += value; } + remove { _onDeactivated -= value; } + } public override void OnBackPressed() { @@ -30,6 +44,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..b45ee6e78db 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 { 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 eef763a932b..f8a2ee28947 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 IAvaloniaActivity 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; } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs new file mode 100644 index 00000000000..fb2eae1b529 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs @@ -0,0 +1,23 @@ +using System; + +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 new file mode 100644 index 00000000000..6d72b56921b --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs @@ -0,0 +1,25 @@ +namespace Avalonia.Controls.ApplicationLifetimes; + +public enum ActivationKind +{ + /// + /// 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/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..fbdfe3aa7d7 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/IActivatableApplicationLifetime.cs @@ -0,0 +1,35 @@ +using System; + +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; + + /// + /// Tells the application that it should attempt to leave its background state. + /// For example on OSX this would be [NSApp unhide] + /// + /// true if it was possible and the platform supports this. false otherwise + public bool TryLeaveBackground(); + + /// + /// Tells the application that it should attempt to enter its background state. + /// For example on OSX this would be [NSApp hide] + /// + /// true if it was possible and the platform supports this. false otherwise + public bool TryEnterBackground(); +} 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.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/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index ae24d06badb..3365da7d360 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -13,6 +13,38 @@ 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() + { + if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.RaiseActivated(ActivationKind.Reopen); + } + } + + void IAvnApplicationEvents.OnHide() + { + if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.RaiseDeactivated(ActivationKind.Background); + } + } + + void IAvnApplicationEvents.OnUnhide() + { + if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.RaiseActivated(ActivationKind.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..caf6e03ec8f --- /dev/null +++ b/src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs @@ -0,0 +1,50 @@ +using System; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Platform; + +namespace Avalonia.Native; + +#nullable enable + +internal class MacOSClassicDesktopStyleApplicationLifetime : ClassicDesktopStyleApplicationLifetime, + IActivatableApplicationLifetime +{ + /// + public event EventHandler? Activated; + + /// + public event EventHandler? Deactivated; + + /// + public bool TryLeaveBackground() + { + var nativeApplicationCommands = AvaloniaLocator.Current.GetService(); + nativeApplicationCommands?.ShowApp(); + + return true; + } + + /// + public bool TryEnterBackground() + { + var nativeApplicationCommands = AvaloniaLocator.Current.GetService(); + nativeApplicationCommands?.HideApp(); + + return true; + } + + internal void RaiseUrl(Uri uri) + { + Activated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, uri)); + } + + internal void RaiseActivated(ActivationKind kind) + { + Activated?.Invoke(this, new ActivatedEventArgs(kind)); + } + + internal void RaiseDeactivated(ActivationKind kind) + { + Deactivated?.Invoke(this, new ActivatedEventArgs(kind)); + } +} 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 279d2d82cdc..2219640f8e8 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1086,11 +1086,15 @@ interface IAvnApplicationEvents : IUnknown { void FilesOpened (IAvnStringArray* urls); bool TryShutdown(); + void OnReopen (); + void OnHide (); + void OnUnhide (); } [uuid(b4284791-055b-4313-8c2e-50f0a8c72ce9)] interface IAvnApplicationCommands : IUnknown { + HRESULT UnhideApp(); HRESULT HideApp(); HRESULT ShowAll(); HRESULT HideOthers(); diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index dbc5f9f4a9b..fbe3cdd3441 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -73,8 +73,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..f534c3f27bb 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -4,11 +4,22 @@ 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)); + }); + } + public AvaloniaView? View; public Control? MainView @@ -33,4 +44,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 76dac84bca1..7f294e155ec 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -50,7 +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 27a1d998fd8..e03aa9e5323 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -245,6 +245,32 @@ export class InputHelper { return pointerEvent.getCoalescedEvents(); } + public static subscribeKeyboardGeometryChange( + element: HTMLInputElement, + handler: (args: any) => boolean) { + if ("virtualKeyboard" in navigator) { + // (navigator as any).virtualKeyboard.overlaysContent = true; + (navigator as any).virtualKeyboard.addEventListener("geometrychange", (event: any) => { + const elementRect = element.getBoundingClientRect(); + const keyboardRect = event.target.boundingRect as DOMRect; + handler({ + x: keyboardRect.x - elementRect.x, + y: keyboardRect.y - elementRect.y, + width: keyboardRect.width, + height: keyboardRect.height + }); + }); + } + } + + 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 = ""; } 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; +}