Skip to content

Feature: Added an icon to the system tray #14285

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

Merged
merged 18 commits into from
Dec 27, 2023
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
38 changes: 27 additions & 11 deletions src/Files.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace Files.App
/// </summary>
public partial class App : Application
{
private static SystemTrayIcon? SystemTrayIcon { get; set; }

public static TaskCompletionSource? SplashScreenLoadingTCS { get; private set; }
public static string? OutputPath { get; set; }

Expand Down Expand Up @@ -55,8 +57,7 @@ public App()
}

/// <summary>
/// Invoked when the application is launched normally by the end user.
/// Other entry points will be used such as when the application is launched to open a specific file.
/// Gets invoked when the application is launched normally by the end user.
/// </summary>
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Expand Down Expand Up @@ -108,13 +109,16 @@ async Task ActivateAsync()
await SplashScreenLoadingTCS!.Task.WithTimeoutAsync(TimeSpan.FromMilliseconds(500));
SplashScreenLoadingTCS = null;

// Create a system tray icon
SystemTrayIcon = new SystemTrayIcon().Show();

_ = AppLifecycleHelper.InitializeAppComponentsAsync();
_ = MainWindow.Instance.InitializeApplicationAsync(appActivationArguments.Data);
}
}

/// <summary>
/// Invoked when the application is activated.
/// Gets invoked when the application is activated.
/// </summary>
public async Task OnActivatedAsync(AppActivationArguments activatedEventArgs)
{
Expand All @@ -126,7 +130,7 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(()
}

/// <summary>
/// Invoked when the main window is activated.
/// Gets invoked when the main window is activated.
/// </summary>
private void Window_Activated(object sender, WindowActivatedEventArgs args)
{
Expand All @@ -135,12 +139,15 @@ private void Window_Activated(object sender, WindowActivatedEventArgs args)
args.WindowActivationState != WindowActivationState.PointerActivated)
return;

ApplicationData.Current.LocalSettings.Values["INSTANCE_ACTIVE"] = -Process.GetCurrentProcess().Id;
ApplicationData.Current.LocalSettings.Values["INSTANCE_ACTIVE"] = -Environment.ProcessId;
}

/// <summary>
/// Invoked when application execution is being closed. Save application state.
/// Gets invoked when the application execution is closed.
/// </summary>
/// <remarks>
/// Saves the current state of the app such as opened tabs, and disposes all cached resources.
/// </remarks>
private async void Window_Closed(object sender, WindowEventArgs args)
{
// Save application state and stop any background activity
Expand All @@ -156,9 +163,10 @@ private async void Window_Closed(object sender, WindowEventArgs args)
return;
}

// Continue running the app on the background
if (userSettingsService.GeneralSettingsService.LeaveAppRunning &&
!AppModel.ForceProcessTermination &&
!Process.GetProcessesByName("Files").Any(x => x.Id != Process.GetCurrentProcess().Id))
!Process.GetProcessesByName("Files").Any(x => x.Id != Environment.ProcessId))
{
// Close open content dialogs
UIHelpers.CloseAllDialogs();
Expand All @@ -168,7 +176,6 @@ private async void Window_Closed(object sender, WindowEventArgs args)

// Cache the window instead of closing it
MainWindow.Instance.AppWindow.Hide();
args.Handled = true;

// Save and close all tabs
AppLifecycleHelper.SaveSessionTabs();
Expand All @@ -180,16 +187,22 @@ private async void Window_Closed(object sender, WindowEventArgs args)

// Sleep current instance
Program.Pool = new(0, 1, $"Files-{ApplicationService.AppEnvironment}-Instance");

Thread.Yield();

if (Program.Pool.WaitOne())
{
// Resume the instance
Program.Pool.Dispose();
Program.Pool = null;

_ = AppLifecycleHelper.CheckAppUpdate();
if (!AppModel.ForceProcessTermination)
{
args.Handled = true;
_ = AppLifecycleHelper.CheckAppUpdate();
return;
}
}

return;
}

// Method can take a long time, make sure the window is hidden
Expand Down Expand Up @@ -237,6 +250,9 @@ await SafetyExtensions.IgnoreExceptions(async () =>
FileOperationsHelpers.WaitForCompletion();
}

/// <summary>
/// Gets invoked when the last opened flyout is closed.
/// </summary>
private static void LastOpenedFlyout_Closed(object? sender, object e)
{
if (sender is not CommandBarFlyout commandBarFlyout)
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/Files.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
<PackageReference Include="Vanara.Windows.Shell" Version="3.4.17" />
<PackageReference Include="Microsoft.Management.Infrastructure" Version="3.0.0" />
<PackageReference Include="Microsoft.Management.Infrastructure.Runtime.Win" Version="3.0.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.1.647-beta" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
global using global::Files.App.Utils.Shell;
global using global::Files.App.Utils.StatusCenter;
global using global::Files.App.Utils.Storage;
global using global::Files.App.Utils.Taskbar;
global using global::Files.App.Data.Attributes;
global using global::Files.App.Data.Behaviors;
global using global::Files.App.Data.Commands;
Expand Down
28 changes: 28 additions & 0 deletions src/Files.App/NativeMethods.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.
{
"$schema": "https://aka.ms/CsWin32.schema.json",

// Emit COM interfaces instead of structs, and allow generation of non-blittable structs for the sake of an easier to use API.
"allowMarshaling": true,

// A value indicating whether to generate APIs judged to be unnecessary or redundant given the target framework.
// This is useful for multi-targeting projects that need a consistent set of APIs across target frameworks
// to avoid too many conditional compilation regions.
"multiTargetingFriendlyAPIs": false,

// A value indicating whether friendly overloads should use safe handles.
"useSafeHandles": true,

// Omit ANSI functions and remove `W` suffix from UTF-16 functions.
"wideCharOnly": true,

// A value indicating whether to emit a single source file as opposed to types spread across many files.
"emitSingleFile": false,

// The name of a single class under which all p/invoke methods and constants are generated, regardless of imported module.
"className": "PInvoke",

// A value indicating whether to expose the generated APIs publicly (as opposed to internally).
"public": true
}
33 changes: 33 additions & 0 deletions src/Files.App/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

WNDPROC
WNDCLASSEXW
RegisterClassEx
CreateWindowEx
DestroyWindow
GetModuleHandle
RECT
NOTIFYICONIDENTIFIER
Shell_NotifyIconGetRect
RegisterWindowMessage
NOTIFYICONDATAW
Shell_NotifyIcon
GetCursorPos
DestroyMenu
AppendMenu
CreatePopupMenu
SetForegroundWindow
TrackPopupMenuEx
TRACK_POPUP_MENU_FLAGS
GetSystemMetricsForDpi
DefWindowProc
SYSTEM_METRICS_INDEX
GetDpiForWindow
HWND
LRESULT
WPARAM
LPARAM
WM_LBUTTONUP
WM_RBUTTONUP
WM_DESTROY
87 changes: 53 additions & 34 deletions src/Files.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,46 @@

namespace Files.App
{
internal class Program
/// <summary>
/// Represents the base entry point of the Files app.
/// </summary>
/// <remarks>
/// Gets called at the first time when the app launched or activated.
/// </remarks>
internal sealed class Program
{
public static Semaphore Pool;
private const uint CWMO_DEFAULT = 0;
private const uint INFINITE = 0xFFFFFFFF;

public static Semaphore? Pool { get; set; }

static Program()
{
Pool = new(0, 1, $"Files-{ApplicationService.AppEnvironment}-Instance", out var isNew);
var pool = new Semaphore(0, 1, $"Files-{ApplicationService.AppEnvironment}-Instance", out var isNew);

if (!isNew)
{
// Resume cached instance
Pool.Release();
pool.Release();

// Redirect to the main process
var activePid = ApplicationData.Current.LocalSettings.Values.Get("INSTANCE_ACTIVE", -1);
var instance = AppInstance.FindOrRegisterForKey(activePid.ToString());
RedirectActivationTo(instance, AppInstance.GetCurrent().GetActivatedEventArgs());

// Kill the current process
Environment.Exit(0);
}
Pool.Dispose();

pool.Dispose();
}

// Note:
// We can't declare Main to be async because in a WinUI app
// This prevents Narrator from reading XAML elements
// https://github.com/microsoft/WindowsAppSDK-Samples/blob/main/Samples/AppLifecycle/Instancing/cs-winui-packaged/CsWinUiDesktopInstancing/CsWinUiDesktopInstancing/Program.cs
// STAThread has no effect if main is async, needed for Clipboard
/// <summary>
/// Initializes the process; the entry point of the process.
/// </summary>
/// <remarks>
/// <see cref="Main"/> cannot be declared to be async because this prevents Narrator from reading XAML elements in a WinUI app.
/// </remarks>
[STAThread]
private static void Main()
{
Expand Down Expand Up @@ -143,34 +159,35 @@ private static void Main()

var currentInstance = AppInstance.FindOrRegisterForKey((-proc.Id).ToString());
if (currentInstance.IsCurrent)
{
currentInstance.Activated += OnActivated;
}

ApplicationData.Current.LocalSettings.Values["INSTANCE_ACTIVE"] = -proc.Id;

Application.Start((p) =>
{
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread());
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
new App();

_ = new App();
});
}

/// <summary>
/// Gets invoked when the application is activated.
/// </summary>
private static async void OnActivated(object? sender, AppActivationArguments args)
{
if (App.Current is App thisApp)
{
// WINUI3: Verify if needed or OnLaunched is called
// WINUI3: Verify if needed or OnLaunched is called
if (App.Current is App thisApp)
await thisApp.OnActivatedAsync(args);
}
}

private const uint CWMO_DEFAULT = 0;
private const uint INFINITE = 0xFFFFFFFF;

// Do the redirection on another thread, and use a non-blocking wait method to wait for the redirection to complete
/// <summary>
/// Redirects the activation to the main process.
/// </summary>
/// <remarks>
/// Redirects on another thread and uses a non-blocking wait method to wait for the redirection to complete.
/// </remarks>
public static void RedirectActivationTo(AppInstance keyInstance, AppActivationArguments args)
{
IntPtr eventHandle = CreateEvent(IntPtr.Zero, true, false, null);
Expand All @@ -182,15 +199,17 @@ public static void RedirectActivationTo(AppInstance keyInstance, AppActivationAr
});

_ = CoWaitForMultipleObjects(
CWMO_DEFAULT,
INFINITE,
1,
new IntPtr[] { eventHandle },
out uint handleIndex);
CWMO_DEFAULT,
INFINITE,
1,
new IntPtr[] { eventHandle },
out uint handleIndex);
}

public static void OpenShellCommandInExplorer(string shellCommand, int pid)
=> Win32API.OpenFolderInExistingShellWindow(shellCommand);
{
Win32API.OpenFolderInExistingShellWindow(shellCommand);
}

public static void OpenFileFromTile(string filePath)
{
Expand All @@ -203,11 +222,11 @@ public static void OpenFileFromTile(string filePath)
});

_ = CoWaitForMultipleObjects(
CWMO_DEFAULT,
INFINITE,
1,
new IntPtr[] { eventHandle },
out uint handleIndex);
CWMO_DEFAULT,
INFINITE,
1,
new IntPtr[] { eventHandle },
out uint handleIndex);
}
}
}
6 changes: 6 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3662,6 +3662,12 @@
<data name="FailedToRotateImage" xml:space="preserve">
<value>Failed to rotate the image</value>
</data>
<data name="Restart" xml:space="preserve">
<value>Restart</value>
</data>
<data name="Quit" xml:space="preserve">
<value>Quit</value>
</data>
<data name="FaildToShareItems" xml:space="preserve">
<value>Failed to share items</value>
</data>
Expand Down
Loading