Skip to content

Commit 3752486

Browse files
authored
Feature: Added an icon to the system tray (#14285)
1 parent d15f8f1 commit 3752486

File tree

9 files changed

+588
-45
lines changed

9 files changed

+588
-45
lines changed

src/Files.App/App.xaml.cs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace Files.App
1717
/// </summary>
1818
public partial class App : Application
1919
{
20+
private static SystemTrayIcon? SystemTrayIcon { get; set; }
21+
2022
public static TaskCompletionSource? SplashScreenLoadingTCS { get; private set; }
2123
public static string? OutputPath { get; set; }
2224

@@ -55,8 +57,7 @@ public App()
5557
}
5658

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

112+
// Create a system tray icon
113+
SystemTrayIcon = new SystemTrayIcon().Show();
114+
111115
_ = AppLifecycleHelper.InitializeAppComponentsAsync();
112116
_ = MainWindow.Instance.InitializeApplicationAsync(appActivationArguments.Data);
113117
}
114118
}
115119

116120
/// <summary>
117-
/// Invoked when the application is activated.
121+
/// Gets invoked when the application is activated.
118122
/// </summary>
119123
public async Task OnActivatedAsync(AppActivationArguments activatedEventArgs)
120124
{
@@ -126,7 +130,7 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(()
126130
}
127131

128132
/// <summary>
129-
/// Invoked when the main window is activated.
133+
/// Gets invoked when the main window is activated.
130134
/// </summary>
131135
private void Window_Activated(object sender, WindowActivatedEventArgs args)
132136
{
@@ -135,12 +139,15 @@ private void Window_Activated(object sender, WindowActivatedEventArgs args)
135139
args.WindowActivationState != WindowActivationState.PointerActivated)
136140
return;
137141

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

141145
/// <summary>
142-
/// Invoked when application execution is being closed. Save application state.
146+
/// Gets invoked when the application execution is closed.
143147
/// </summary>
148+
/// <remarks>
149+
/// Saves the current state of the app such as opened tabs, and disposes all cached resources.
150+
/// </remarks>
144151
private async void Window_Closed(object sender, WindowEventArgs args)
145152
{
146153
// Save application state and stop any background activity
@@ -156,9 +163,10 @@ private async void Window_Closed(object sender, WindowEventArgs args)
156163
return;
157164
}
158165

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

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

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

181188
// Sleep current instance
182189
Program.Pool = new(0, 1, $"Files-{ApplicationService.AppEnvironment}-Instance");
190+
183191
Thread.Yield();
192+
184193
if (Program.Pool.WaitOne())
185194
{
186195
// Resume the instance
187196
Program.Pool.Dispose();
197+
Program.Pool = null;
188198

189-
_ = AppLifecycleHelper.CheckAppUpdate();
199+
if (!AppModel.ForceProcessTermination)
200+
{
201+
args.Handled = true;
202+
_ = AppLifecycleHelper.CheckAppUpdate();
203+
return;
204+
}
190205
}
191-
192-
return;
193206
}
194207

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

253+
/// <summary>
254+
/// Gets invoked when the last opened flyout is closed.
255+
/// </summary>
240256
private static void LastOpenedFlyout_Closed(object? sender, object e)
241257
{
242258
if (sender is not CommandBarFlyout commandBarFlyout)

src/Files.App/Files.App.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
<PackageReference Include="Vanara.Windows.Shell" Version="3.4.17" />
9999
<PackageReference Include="Microsoft.Management.Infrastructure" Version="3.0.0" />
100100
<PackageReference Include="Microsoft.Management.Infrastructure.Runtime.Win" Version="3.0.0" />
101+
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.1.647-beta" PrivateAssets="all" />
101102
</ItemGroup>
102103

103104
<ItemGroup>

src/Files.App/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
global using global::Files.App.Utils.Shell;
3535
global using global::Files.App.Utils.StatusCenter;
3636
global using global::Files.App.Utils.Storage;
37+
global using global::Files.App.Utils.Taskbar;
3738
global using global::Files.App.Data.Attributes;
3839
global using global::Files.App.Data.Behaviors;
3940
global using global::Files.App.Data.Commands;

src/Files.App/NativeMethods.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
{
4+
"$schema": "https://aka.ms/CsWin32.schema.json",
5+
6+
// Emit COM interfaces instead of structs, and allow generation of non-blittable structs for the sake of an easier to use API.
7+
"allowMarshaling": true,
8+
9+
// A value indicating whether to generate APIs judged to be unnecessary or redundant given the target framework.
10+
// This is useful for multi-targeting projects that need a consistent set of APIs across target frameworks
11+
// to avoid too many conditional compilation regions.
12+
"multiTargetingFriendlyAPIs": false,
13+
14+
// A value indicating whether friendly overloads should use safe handles.
15+
"useSafeHandles": true,
16+
17+
// Omit ANSI functions and remove `W` suffix from UTF-16 functions.
18+
"wideCharOnly": true,
19+
20+
// A value indicating whether to emit a single source file as opposed to types spread across many files.
21+
"emitSingleFile": false,
22+
23+
// The name of a single class under which all p/invoke methods and constants are generated, regardless of imported module.
24+
"className": "PInvoke",
25+
26+
// A value indicating whether to expose the generated APIs publicly (as opposed to internally).
27+
"public": true
28+
}

src/Files.App/NativeMethods.txt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
WNDPROC
5+
WNDCLASSEXW
6+
RegisterClassEx
7+
CreateWindowEx
8+
DestroyWindow
9+
GetModuleHandle
10+
RECT
11+
NOTIFYICONIDENTIFIER
12+
Shell_NotifyIconGetRect
13+
RegisterWindowMessage
14+
NOTIFYICONDATAW
15+
Shell_NotifyIcon
16+
GetCursorPos
17+
DestroyMenu
18+
AppendMenu
19+
CreatePopupMenu
20+
SetForegroundWindow
21+
TrackPopupMenuEx
22+
TRACK_POPUP_MENU_FLAGS
23+
GetSystemMetricsForDpi
24+
DefWindowProc
25+
SYSTEM_METRICS_INDEX
26+
GetDpiForWindow
27+
HWND
28+
LRESULT
29+
WPARAM
30+
LPARAM
31+
WM_LBUTTONUP
32+
WM_RBUTTONUP
33+
WM_DESTROY

src/Files.App/Program.cs

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,46 @@
1212

1313
namespace Files.App
1414
{
15-
internal class Program
15+
/// <summary>
16+
/// Represents the base entry point of the Files app.
17+
/// </summary>
18+
/// <remarks>
19+
/// Gets called at the first time when the app launched or activated.
20+
/// </remarks>
21+
internal sealed class Program
1622
{
17-
public static Semaphore Pool;
23+
private const uint CWMO_DEFAULT = 0;
24+
private const uint INFINITE = 0xFFFFFFFF;
25+
26+
public static Semaphore? Pool { get; set; }
1827

1928
static Program()
2029
{
21-
Pool = new(0, 1, $"Files-{ApplicationService.AppEnvironment}-Instance", out var isNew);
30+
var pool = new Semaphore(0, 1, $"Files-{ApplicationService.AppEnvironment}-Instance", out var isNew);
31+
2232
if (!isNew)
2333
{
2434
// Resume cached instance
25-
Pool.Release();
35+
pool.Release();
36+
37+
// Redirect to the main process
2638
var activePid = ApplicationData.Current.LocalSettings.Values.Get("INSTANCE_ACTIVE", -1);
2739
var instance = AppInstance.FindOrRegisterForKey(activePid.ToString());
2840
RedirectActivationTo(instance, AppInstance.GetCurrent().GetActivatedEventArgs());
41+
42+
// Kill the current process
2943
Environment.Exit(0);
3044
}
31-
Pool.Dispose();
45+
46+
pool.Dispose();
3247
}
3348

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

144160
var currentInstance = AppInstance.FindOrRegisterForKey((-proc.Id).ToString());
145161
if (currentInstance.IsCurrent)
146-
{
147162
currentInstance.Activated += OnActivated;
148-
}
149163

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

152166
Application.Start((p) =>
153167
{
154-
var context = new DispatcherQueueSynchronizationContext(
155-
DispatcherQueue.GetForCurrentThread());
168+
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
156169
SynchronizationContext.SetSynchronizationContext(context);
157-
new App();
170+
171+
_ = new App();
158172
});
159173
}
160174

175+
/// <summary>
176+
/// Gets invoked when the application is activated.
177+
/// </summary>
161178
private static async void OnActivated(object? sender, AppActivationArguments args)
162179
{
163-
if (App.Current is App thisApp)
164-
{
165-
// WINUI3: Verify if needed or OnLaunched is called
180+
// WINUI3: Verify if needed or OnLaunched is called
181+
if (App.Current is App thisApp)
166182
await thisApp.OnActivatedAsync(args);
167-
}
168183
}
169184

170-
private const uint CWMO_DEFAULT = 0;
171-
private const uint INFINITE = 0xFFFFFFFF;
172-
173-
// Do the redirection on another thread, and use a non-blocking wait method to wait for the redirection to complete
185+
/// <summary>
186+
/// Redirects the activation to the main process.
187+
/// </summary>
188+
/// <remarks>
189+
/// Redirects on another thread and uses a non-blocking wait method to wait for the redirection to complete.
190+
/// </remarks>
174191
public static void RedirectActivationTo(AppInstance keyInstance, AppActivationArguments args)
175192
{
176193
IntPtr eventHandle = CreateEvent(IntPtr.Zero, true, false, null);
@@ -182,15 +199,17 @@ public static void RedirectActivationTo(AppInstance keyInstance, AppActivationAr
182199
});
183200

184201
_ = CoWaitForMultipleObjects(
185-
CWMO_DEFAULT,
186-
INFINITE,
187-
1,
188-
new IntPtr[] { eventHandle },
189-
out uint handleIndex);
202+
CWMO_DEFAULT,
203+
INFINITE,
204+
1,
205+
new IntPtr[] { eventHandle },
206+
out uint handleIndex);
190207
}
191208

192209
public static void OpenShellCommandInExplorer(string shellCommand, int pid)
193-
=> Win32API.OpenFolderInExistingShellWindow(shellCommand);
210+
{
211+
Win32API.OpenFolderInExistingShellWindow(shellCommand);
212+
}
194213

195214
public static void OpenFileFromTile(string filePath)
196215
{
@@ -203,11 +222,11 @@ public static void OpenFileFromTile(string filePath)
203222
});
204223

205224
_ = CoWaitForMultipleObjects(
206-
CWMO_DEFAULT,
207-
INFINITE,
208-
1,
209-
new IntPtr[] { eventHandle },
210-
out uint handleIndex);
225+
CWMO_DEFAULT,
226+
INFINITE,
227+
1,
228+
new IntPtr[] { eventHandle },
229+
out uint handleIndex);
211230
}
212231
}
213232
}

src/Files.App/Strings/en-US/Resources.resw

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3662,6 +3662,12 @@
36623662
<data name="FailedToRotateImage" xml:space="preserve">
36633663
<value>Failed to rotate the image</value>
36643664
</data>
3665+
<data name="Restart" xml:space="preserve">
3666+
<value>Restart</value>
3667+
</data>
3668+
<data name="Quit" xml:space="preserve">
3669+
<value>Quit</value>
3670+
</data>
36653671
<data name="FaildToShareItems" xml:space="preserve">
36663672
<value>Failed to share items</value>
36673673
</data>

0 commit comments

Comments
 (0)