Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an event so that users can detect when an Application icon is clicked #14106

Merged
merged 14 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -101,6 +101,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 @@ -36,7 +36,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 @@ -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, 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 @@ -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);
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should split these args, and add DeactivatedEventArgs instead of reusing. Maybe even with DeactivationKind.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I interpreted correctly what @danwalmsley meant if ActivationKind.Background occurs in the Activated event, it indicates that the app has been reactivated from suspension, otherwise in Deactivated it indicates that the app has been suspended. So for each of the elements of the enum.

If so I would suggest renaming:
ActivatedEventArgs in ApplicationStateEventArgs
ActivationKind in ApplicationStateKind

and create

ApplicationStateActivatedEventArgs: ApplicationStateEventArgs
ApplicationStateDeactivatedEventArgs: ApplicationStateEventArgs

cc @robloo


/// <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
Loading