Skip to content

Commit

Permalink
Feature: Added an icon to the system tray (files-community#14285)
Browse files Browse the repository at this point in the history
  • Loading branch information
0x5bfa authored Dec 27, 2023
1 parent d15f8f1 commit 3752486
Show file tree
Hide file tree
Showing 9 changed files with 588 additions and 45 deletions.
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

0 comments on commit 3752486

Please sign in to comment.