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 9 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
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
50 changes: 50 additions & 0 deletions src/Avalonia.Native/MacOSClassicDesktopStyleApplicationLifetime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;

namespace Avalonia.Native;

#nullable enable

internal class MacOSClassicDesktopStyleApplicationLifetime : ClassicDesktopStyleApplicationLifetime,
IActivatableApplicationLifetime
{
/// <inheritdoc />
public event EventHandler<ActivatedEventArgs>? Activated;

/// <inheritdoc />
public event EventHandler<ActivatedEventArgs>? Deactivated;

/// <inheritdoc />
public bool TryLeaveBackground()
{
var nativeApplicationCommands = AvaloniaLocator.Current.GetService<INativeApplicationCommands>();
nativeApplicationCommands?.ShowApp();

return true;
}

/// <inheritdoc />
public bool TryEnterBackground()
{
var nativeApplicationCommands = AvaloniaLocator.Current.GetService<INativeApplicationCommands>();
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));
}
}
6 changes: 5 additions & 1 deletion src/Avalonia.Native/MacOSNativeMenuCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public MacOSNativeMenuCommands(IAvnApplicationCommands commands)
_commands = commands;
}

public void ShowApp()
{
_commands.UnhideApp();
}

public void HideApp()
{
_commands.HideApp();
Expand All @@ -28,7 +33,6 @@ public void HideOthers()
_commands.HideOthers();
}


public static readonly AttachedProperty<bool> IsServicesSubmenuProperty =
AvaloniaProperty.RegisterAttached<MacOSNativeMenuCommands, NativeMenu, bool>("IsServicesSubmenu", false);
}
Expand Down
4 changes: 4 additions & 0 deletions src/Avalonia.Native/avn.idl
Original file line number Diff line number Diff line change
Expand Up @@ -1087,11 +1087,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();
Expand Down