[CmdPal] Tray icon settings (#38672)
Some checks are pending
Spell checking / Check Spelling (push) Waiting to run
Spell checking / Report (Push) (push) Blocked by required conditions
Spell checking / Report (PR) (push) Blocked by required conditions
Spell checking / Update PR (push) Waiting to run

<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? -->
## Summary of the Pull Request

Added a settings to enable/disable the system tray icon (enabled by default).
Adopter the term "system tray icon" for consistency with Windows 11 settings.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] **Closes:** #38407
This commit is contained in:
Davide Giacometti
2025-04-10 23:44:54 +02:00
committed by GitHub
parent a7994402fe
commit 55f8f3a53e
5 changed files with 60 additions and 34 deletions

View File

@@ -36,6 +36,8 @@ public partial class SettingsModel : ObservableObject
public bool HighlightSearchOnActivate { get; set; } = true; public bool HighlightSearchOnActivate { get; set; } = true;
public bool ShowSystemTrayIcon { get; set; } = true;
public Dictionary<string, ProviderSettings> ProviderSettings { get; set; } = []; public Dictionary<string, ProviderSettings> ProviderSettings { get; set; } = [];
public Dictionary<string, CommandAlias> Aliases { get; set; } = []; public Dictionary<string, CommandAlias> Aliases { get; set; } = [];

View File

@@ -87,6 +87,16 @@ public partial class SettingsViewModel : INotifyPropertyChanged
} }
} }
public bool ShowSystemTrayIcon
{
get => _settings.ShowSystemTrayIcon;
set
{
_settings.ShowSystemTrayIcon = value;
Save();
}
}
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = []; public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler) public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)

View File

@@ -54,7 +54,6 @@ public sealed partial class MainWindow : Window,
// Notification Area ("Tray") icon data // Notification Area ("Tray") icon data
private NOTIFYICONDATAW? _trayIconData; private NOTIFYICONDATAW? _trayIconData;
private bool _createdIcon;
private DestroyIconSafeHandle? _largeIcon; private DestroyIconSafeHandle? _largeIcon;
private DesktopAcrylicController? _acrylicController; private DesktopAcrylicController? _acrylicController;
@@ -99,7 +98,6 @@ public sealed partial class MainWindow : Window,
_hotkeyWndProc = HotKeyPrc; _hotkeyWndProc = HotKeyPrc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc); var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer)); _originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
AddNotificationIcon();
// Load our settings, and then also wire up a settings changed handler // Load our settings, and then also wire up a settings changed handler
HotReloadSettings(); HotReloadSettings();
@@ -149,6 +147,7 @@ public sealed partial class MainWindow : Window,
var settings = App.Current.Services.GetService<SettingsModel>()!; var settings = App.Current.Services.GetService<SettingsModel>()!;
SetupHotkey(settings); SetupHotkey(settings);
SetupTrayIcon(settings.ShowSystemTrayIcon);
// This will prevent our window from appearing in alt+tab or the taskbar. // This will prevent our window from appearing in alt+tab or the taskbar.
// You'll _need_ to use the hotkey to summon it. // You'll _need_ to use the hotkey to summon it.
@@ -299,7 +298,7 @@ public sealed partial class MainWindow : Window,
var extensionService = serviceProvider.GetService<IExtensionService>()!; var extensionService = serviceProvider.GetService<IExtensionService>()!;
extensionService.SignalStopExtensionsAsync(); extensionService.SignalStopExtensionsAsync();
RemoveNotificationIcon(); RemoveTrayIcon();
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592). // WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
// Workaround by turning it off before shutdown. // Workaround by turning it off before shutdown.
@@ -491,9 +490,9 @@ public sealed partial class MainWindow : Window,
// WM_WINDOWPOSCHANGING which is always received on explorer startup sequence. // WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
case PInvoke.WM_WINDOWPOSCHANGING: case PInvoke.WM_WINDOWPOSCHANGING:
{ {
if (!_createdIcon) if (_trayIconData == null)
{ {
AddNotificationIcon(); SetupTrayIcon();
} }
} }
@@ -505,7 +504,7 @@ public sealed partial class MainWindow : Window,
{ {
// Handle the case where explorer.exe restarts. // Handle the case where explorer.exe restarts.
// Even if we created it before, do it again // Even if we created it before, do it again
AddNotificationIcon(); SetupTrayIcon();
} }
else if (uMsg == WM_TRAY_ICON) else if (uMsg == WM_TRAY_ICON)
{ {
@@ -525,55 +524,60 @@ public sealed partial class MainWindow : Window,
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam); return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
} }
private void AddNotificationIcon() private void SetupTrayIcon(bool? showSystemTrayIcon = null)
{ {
// We only need to build the tray data once. if (showSystemTrayIcon ?? App.Current.Services.GetService<SettingsModel>()!.ShowSystemTrayIcon)
if (_trayIconData == null)
{ {
// We need to stash this handle, so it doesn't clean itself up. If // We only need to build the tray data once.
// explorer restarts, we'll come back through here, and we don't if (_trayIconData == null)
// really need to re-load the icon in that case. We can just use
// the handle from the first time.
_largeIcon = GetAppIconHandle();
_trayIconData = new NOTIFYICONDATAW()
{ {
cbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATAW)), // We need to stash this handle, so it doesn't clean itself up. If
hWnd = _hwnd, // explorer restarts, we'll come back through here, and we don't
uID = MY_NOTIFY_ID, // really need to re-load the icon in that case. We can just use
uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_ICON | NOTIFY_ICON_DATA_FLAGS.NIF_TIP, // the handle from the first time.
uCallbackMessage = WM_TRAY_ICON, _largeIcon = GetAppIconHandle();
hIcon = (HICON)_largeIcon.DangerousGetHandle(), _trayIconData = new NOTIFYICONDATAW()
szTip = RS_.GetString("AppStoreName"), {
}; cbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATAW)),
hWnd = _hwnd,
uID = MY_NOTIFY_ID,
uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_ICON | NOTIFY_ICON_DATA_FLAGS.NIF_TIP,
uCallbackMessage = WM_TRAY_ICON,
hIcon = (HICON)_largeIcon.DangerousGetHandle(),
szTip = RS_.GetString("AppStoreName"),
};
}
var d = (NOTIFYICONDATAW)_trayIconData;
// Add the notification icon
PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d);
} }
else
var d = (NOTIFYICONDATAW)_trayIconData;
// Add the notification icon
if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d))
{ {
_createdIcon = true; RemoveTrayIcon();
} }
} }
private void RemoveNotificationIcon() private void RemoveTrayIcon()
{ {
if (_trayIconData != null && _createdIcon) if (_trayIconData != null)
{ {
var d = (NOTIFYICONDATAW)_trayIconData; var d = (NOTIFYICONDATAW)_trayIconData;
if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d)) if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d))
{ {
_createdIcon = false; _trayIconData = null;
} }
} }
_largeIcon?.Close();
} }
private DestroyIconSafeHandle GetAppIconHandle() private DestroyIconSafeHandle GetAppIconHandle()
{ {
var exePath = System.Reflection.Assembly.GetExecutingAssembly().Location; var exePath = System.Reflection.Assembly.GetExecutingAssembly().Location;
DestroyIconSafeHandle largeIcon; DestroyIconSafeHandle largeIcon;
DestroyIconSafeHandle smallIcon; PInvoke.ExtractIconEx(exePath, 0, out largeIcon, out _, 1);
PInvoke.ExtractIconEx(exePath, 0, out largeIcon, out smallIcon, 1);
return largeIcon; return largeIcon;
} }
} }

View File

@@ -68,6 +68,10 @@
<ToggleSwitch IsOn="{x:Bind viewModel.SingleClickActivates, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind viewModel.SingleClickActivates, Mode=TwoWay}" />
</controls:SettingsCard> </controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE75B;}">
<ToggleSwitch IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Example 'About' section --> <!-- Example 'About' section -->
<TextBlock x:Uid="AboutSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock x:Uid="AboutSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />

View File

@@ -385,4 +385,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="BehaviorSettingsHeader.Text" xml:space="preserve"> <data name="BehaviorSettingsHeader.Text" xml:space="preserve">
<value>Behavior</value> <value>Behavior</value>
</data> </data>
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Header" xml:space="preserve">
<value>Show system tray icon</value>
</data>
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Description" xml:space="preserve">
<value>Choose if Command Palette is visible in the system tray</value>
</data>
</root> </root>