Skip to content

Commit

Permalink
Add an event so that users can detect when an Application icon is cli…
Browse files Browse the repository at this point in the history
…cked (#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 <maxkatz6@outlook.com>
#Conflicts:
#	src/Browser/Avalonia.Browser/Interop/InputHelper.cs
#	src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  • Loading branch information
danwalmsley authored and maxkatz6 committed Jan 17, 2024
1 parent 6b5a9a7 commit 8bd4941
Show file tree
Hide file tree
Showing 26 changed files with 438 additions and 25 deletions.
23 changes: 23 additions & 0 deletions native/Avalonia.Native/src/OSX/app.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString *> *)filenames
{
auto array = CreateAvnStringArray(filenames);
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions native/Avalonia.Native/src/OSX/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class AvnApplicationCommands : public ComSingleObject<IAvnApplicationCommands, &
public:
FORWARD_IUNKNOWN()

virtual HRESULT UnhideApp() override;
virtual HRESULT HideApp() override;
virtual HRESULT ShowAll() override;
virtual HRESULT HideOthers() override;
Expand Down
7 changes: 5 additions & 2 deletions samples/ControlCatalog.Android/MainActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<App>
{
protected override Avalonia.AppBuilder CustomizeAppBuilder(Avalonia.AppBuilder builder)
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
.AfterSetup(_ =>
Expand Down
8 changes: 8 additions & 0 deletions samples/ControlCatalog/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
2 changes: 1 addition & 1 deletion src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ private void InitializeApp()
{
var builder = CreateAppBuilder();

builder.SetupWithLifetime(new SingleViewLifetime());
builder.SetupWithLifetime(new SingleViewLifetime(this));

s_appBuilder = builder;
}
Expand Down
40 changes: 39 additions & 1 deletion src/Android/Avalonia.Android/AvaloniaMainActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActivatedEventArgs> _onActivated, _onDeactivated;

public Action<int, Result, Intent> ActivityResult { get; set; }
public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; }

public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
event EventHandler<ActivatedEventArgs> IAvaloniaActivity.Activated
{
add { _onActivated += value; }
remove { _onActivated -= value; }
}

event EventHandler<ActivatedEventArgs> IAvaloniaActivity.Deactivated
{
add { _onDeactivated += value; }
remove { _onDeactivated -= value; }
}

public override void OnBackPressed()
{
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/Android/Avalonia.Android/IAndroidNavigationService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Avalonia.Controls.ApplicationLifetimes;

namespace Avalonia.Android
{
Expand Down
10 changes: 10 additions & 0 deletions src/Android/Avalonia.Android/IAvaloniaActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using Avalonia.Controls.ApplicationLifetimes;

namespace Avalonia.Android;

public interface IAvaloniaActivity : IActivityResultHandler, IActivityNavigationService
{
event EventHandler<ActivatedEventArgs> Activated;
event EventHandler<ActivatedEventArgs> Deactivated;
}
23 changes: 21 additions & 2 deletions src/Android/Avalonia.Android/SingleViewLifetime.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,5 +36,10 @@ public AvaloniaView View
}

public Control MainView { get; set; }
public event EventHandler<ActivatedEventArgs> Activated;
public event EventHandler<ActivatedEventArgs> Deactivated;

public bool TryLeaveBackground() => _activity.MoveTaskToBack(true);
public bool TryEnterBackground() => false;
}
}
23 changes: 23 additions & 0 deletions src/Avalonia.Controls/ApplicationLifetimes/ActivatedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;

namespace Avalonia.Controls.ApplicationLifetimes;

/// <summary>
/// Event args for an Application Lifetime Activated or Deactivated events.
/// </summary>
public class ActivatedEventArgs : EventArgs
{
/// <summary>
/// Ctor for ActivatedEventArgs
/// </summary>
/// <param name="kind">The <see cref="ActivationKind"/> that this event represents</param>
public ActivatedEventArgs(ActivationKind kind)
{
Kind = kind;
}

/// <summary>
/// The <see cref="ActivationKind"/> that this event represents.
/// </summary>
public ActivationKind Kind { get; }
}
25 changes: 25 additions & 0 deletions src/Avalonia.Controls/ApplicationLifetimes/ActivationKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Avalonia.Controls.ApplicationLifetimes;

public enum ActivationKind
{
/// <summary>
/// When the application is passed a URI to open.
/// </summary>
OpenUri = 20,

/// <summary>
/// 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.
/// </summary>
Reopen = 30,

/// <summary>
/// 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.
/// </summary>
Background = 40
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClassicDesktopStyleApplicationLifetime>();

if (lifetime == null)
{
Args = args,
ShutdownMode = shutdownMode
};
lifetime = new ClassicDesktopStyleApplicationLifetime();
}

lifetime.Args = args;
lifetime.ShutdownMode = shutdownMode;

builder.SetupWithLifetime(lifetime);
return lifetime.Start(args);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;

namespace Avalonia.Controls.ApplicationLifetimes;

/// <summary>
/// An interface for ApplicationLifetimes where the application can be Activated and Deactivated.
/// </summary>
public interface IActivatableApplicationLifetime
{
/// <summary>
/// An event that is raised when the application is Activated for various reasons
/// as described by the <see cref="ActivationKind"/> enumeration.
/// </summary>
event EventHandler<ActivatedEventArgs> Activated;

/// <summary>
/// An event that is raised when the application is Deactivated for various reasons
/// as described by the <see cref="ActivationKind"/> enumeration.
/// </summary>
event EventHandler<ActivatedEventArgs> Deactivated;

/// <summary>
/// Tells the application that it should attempt to leave its background state.
/// For example on OSX this would be [NSApp unhide]
/// </summary>
/// <returns>true if it was possible and the platform supports this. false otherwise</returns>
public bool TryLeaveBackground();

/// <summary>
/// Tells the application that it should attempt to enter its background state.
/// For example on OSX this would be [NSApp hide]
/// </summary>
/// <returns>true if it was possible and the platform supports this. false otherwise</returns>
public bool TryEnterBackground();
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Avalonia.Controls.Platform
/// </summary>
internal interface INativeApplicationCommands
{
void ShowApp();
void HideApp();
void ShowAll();
void HideOthers();
Expand Down
32 changes: 32 additions & 0 deletions src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Native;

namespace Avalonia
Expand All @@ -24,6 +25,9 @@ public static AppBuilder UseAvaloniaNative(this AppBuilder builder)
});
});

AvaloniaLocator.CurrentMutable.Bind<ClassicDesktopStyleApplicationLifetime>()
.ToConstant(new MacOSClassicDesktopStyleApplicationLifetime());

return builder;
}
}
Expand Down
Loading

0 comments on commit 8bd4941

Please sign in to comment.