Add system tray icon support for PowerDisplay

Introduced a `TrayIconService` to manage the system tray icon, enabling quick access to settings and exit options. Added a new `ShowSystemTrayIcon` setting to control tray icon visibility, with UI integration in the settings page.

Implemented `SettingsDeepLink` to open PowerDisplay settings directly in the PowerToys Settings UI. Updated `App.xaml.cs` to integrate tray icon lifecycle management and refresh behavior.

Replaced `ManagedCsWin32` with `CsWin32` for Windows API interop. Added localized strings for tray menu options and updated default settings to enable the tray icon by default. Improved resilience by handling `WM_TASKBAR_RESTART` for tray icon recreation.
This commit is contained in:
Yu Leng
2025-11-27 20:57:54 +08:00
parent 9b86aef4b3
commit 7c69874689
13 changed files with 449 additions and 2 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.IO;
namespace PowerDisplay.Helpers
{
public static class SettingsDeepLink
{
public enum SettingsWindow
{
PowerDisplay,
}
public static void OpenSettings(SettingsWindow window, bool mainExecutableIsOnTheParentFolder)
{
try
{
var directoryPath = System.AppContext.BaseDirectory;
if (mainExecutableIsOnTheParentFolder)
{
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
directoryPath = Path.Combine(directoryPath, "..");
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
else
{
// PowerToys.exe is in the same path as the application.
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=PowerDisplay" });
}
catch
{
// Silently ignore errors opening settings
}
}
}
}

View File

@@ -0,0 +1,276 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT.Interop;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Window procedure delegate for handling window messages.
/// Defined locally because CsWin32 generates WNDPROC as internal with allowMarshaling: false.
/// </summary>
/// <param name="hwnd">Handle to the window.</param>
/// <param name="msg">The message.</param>
/// <param name="wParam">Additional message information.</param>
/// <param name="lParam">Additional message information.</param>
/// <returns>The result of the message processing.</returns>
internal delegate LRESULT WndProcDelegate(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam);
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_*")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_*")]
internal sealed partial class TrayIconService
{
private const uint MY_NOTIFY_ID = 1001;
private const uint WM_TRAY_ICON = PInvoke.WM_USER + 1;
private readonly ISettingsUtils _settingsUtils;
private readonly Action _showWindowAction;
private readonly Action _toggleWindowAction;
private readonly Action _exitAction;
private readonly Action _openSettingsAction;
private readonly uint WM_TASKBAR_RESTART;
private Window? _window;
private HWND _hwnd;
private IntPtr _originalWndProc;
private WndProcDelegate? _trayWndProc;
private NOTIFYICONDATAW? _trayIconData;
private DestroyIconSafeHandle? _largeIcon;
private DestroyMenuSafeHandle? _popupMenu;
public TrayIconService(
ISettingsUtils settingsUtils,
Action showWindowAction,
Action toggleWindowAction,
Action exitAction,
Action openSettingsAction)
{
_settingsUtils = settingsUtils;
_showWindowAction = showWindowAction;
_toggleWindowAction = toggleWindowAction;
_exitAction = exitAction;
_openSettingsAction = openSettingsAction;
// TaskbarCreated is the message that's broadcast when explorer.exe
// restarts. We need to know when that happens to be able to bring our
// notification area icon back
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
}
public void SetupTrayIcon(bool? showSystemTrayIcon = null)
{
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
bool shouldShow = showSystemTrayIcon ?? settings.Properties.ShowSystemTrayIcon;
if (shouldShow)
{
if (_window is null)
{
_window = new Window();
_hwnd = new HWND(WindowNative.GetWindowHandle(_window));
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
// member (and instead like, use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our **WindProc will explode**.
_trayWndProc = WindowProc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_trayWndProc);
_originalWndProc = PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer);
}
if (_trayIconData is null)
{
// We need to stash this handle, so it doesn't clean itself up. If
// explorer restarts, we'll come back through here, and we don't
// really need to re-load the icon in that case. We can just use
// the handle from the first time.
_largeIcon = GetAppIconHandle();
unsafe
{
_trayIconData = new NOTIFYICONDATAW()
{
cbSize = (uint)sizeof(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 = GetString("AppName"),
};
}
}
var d = (NOTIFYICONDATAW)_trayIconData;
// Add the notification icon
PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d);
if (_popupMenu is null)
{
_popupMenu = PInvoke.CreatePopupMenu_SafeHandle();
PInvoke.InsertMenu(_popupMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 1, GetString("TrayMenu_Settings"));
PInvoke.InsertMenu(_popupMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 2, GetString("TrayMenu_Exit"));
}
}
else
{
Destroy();
}
}
public void Destroy()
{
if (_trayIconData is not null)
{
var d = (NOTIFYICONDATAW)_trayIconData;
if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d))
{
_trayIconData = null;
}
}
if (_popupMenu is not null)
{
_popupMenu.Close();
_popupMenu = null;
}
if (_largeIcon is not null)
{
_largeIcon.Close();
_largeIcon = null;
}
if (_window is not null)
{
_window.Close();
_window = null;
_hwnd = HWND.Null;
}
}
private static string GetString(string key)
{
try
{
return ResourceLoaderInstance.ResourceLoader.GetString(key);
}
catch
{
// Fallback if resource not found
return key switch
{
"AppName" => "PowerDisplay",
"TrayMenu_Settings" => "Settings",
"TrayMenu_Exit" => "Exit",
_ => key,
};
}
}
private DestroyIconSafeHandle GetAppIconHandle()
{
var exePath = Path.Combine(AppContext.BaseDirectory, "PowerToys.PowerDisplay.exe");
PInvoke.ExtractIconEx(exePath, 0, out var largeIcon, out _, 1);
return largeIcon;
}
private LRESULT WindowProc(
HWND hwnd,
uint uMsg,
WPARAM wParam,
LPARAM lParam)
{
switch (uMsg)
{
case PInvoke.WM_COMMAND:
{
if (wParam == PInvoke.WM_USER + 1)
{
// Settings menu item
Logger.LogInfo("[TrayIcon] Settings menu clicked");
_openSettingsAction?.Invoke();
}
else if (wParam == PInvoke.WM_USER + 2)
{
// Exit menu item
Logger.LogInfo("[TrayIcon] Exit menu clicked");
_exitAction?.Invoke();
}
}
break;
// Shell_NotifyIcon can fail when we invoke it during the time explorer.exe isn't present/ready to handle it.
// We'll also never receive WM_TASKBAR_RESTART message if the first call to Shell_NotifyIcon failed, so we use
// WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
case PInvoke.WM_WINDOWPOSCHANGING:
{
if (_trayIconData is null)
{
SetupTrayIcon();
}
}
break;
default:
// WM_TASKBAR_RESTART isn't a compile-time constant, so we can't
// use it in a case label
if (uMsg == WM_TASKBAR_RESTART)
{
// Handle the case where explorer.exe restarts.
// Even if we created it before, do it again
Logger.LogInfo("[TrayIcon] Taskbar restarted, recreating tray icon");
SetupTrayIcon();
}
else if (uMsg == WM_TRAY_ICON)
{
switch ((uint)lParam.Value)
{
case PInvoke.WM_RBUTTONUP:
{
if (_popupMenu is not null)
{
PInvoke.GetCursorPos(out var cursorPos);
PInvoke.SetForegroundWindow(_hwnd);
PInvoke.TrackPopupMenuEx(_popupMenu, (uint)TRACK_POPUP_MENU_FLAGS.TPM_LEFTALIGN | (uint)TRACK_POPUP_MENU_FLAGS.TPM_BOTTOMALIGN, cursorPos.X, cursorPos.Y, _hwnd, null);
}
}
break;
case PInvoke.WM_LBUTTONUP:
case PInvoke.WM_LBUTTONDBLCLK:
Logger.LogInfo("[TrayIcon] Left click/double click - toggling window");
_toggleWindowAction?.Invoke();
break;
}
}
break;
}
nint result;
unsafe
{
result = CallWindowProcIntPtr(_originalWndProc, (nint)hwnd.Value, uMsg, wParam.Value, lParam.Value);
}
return new LRESULT(result);
}
[LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
private static partial nint CallWindowProcIntPtr(IntPtr lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"public": true,
"allowMarshaling": false
}

View File

@@ -0,0 +1,19 @@
// TrayIcon APIs
Shell_NotifyIcon
WM_USER
WM_WINDOWPOSCHANGING
RegisterWindowMessageW
ExtractIconEx
TRACK_POPUP_MENU_FLAGS
WM_COMMAND
WM_RBUTTONUP
WM_LBUTTONUP
WM_LBUTTONDBLCLK
CreatePopupMenu
TrackPopupMenuEx
InsertMenu
SetWindowLongPtr
CallWindowProc
GetCursorPos
SetForegroundWindow
WNDPROC

View File

@@ -7,6 +7,7 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<RootNamespace>PowerDisplay</RootNamespace> <RootNamespace>PowerDisplay</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\PowerDisplay\PowerDisplay.ico</ApplicationIcon>
<Platforms>x64;ARM64</Platforms> <Platforms>x64;ARM64</Platforms>
<UseWinUI>true</UseWinUI> <UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling> <EnableMsixTooling>true</EnableMsixTooling>
@@ -53,6 +54,9 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" /> <PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<Manifest Include="$(ApplicationManifest)" /> <Manifest Include="$(ApplicationManifest)" />
</ItemGroup> </ItemGroup>
<!-- <!--
@@ -66,7 +70,7 @@
<ItemGroup> <ItemGroup>
<!-- Removed Common.UI dependency - SettingsDeepLink is now implemented locally --> <!-- Removed Common.UI dependency - SettingsDeepLink is now implemented locally -->
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" /> <ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" /> <!-- Removed ManagedCsWin32 - using CsWin32 directly for TrayIcon APIs -->
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" /> <ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" /> <ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\PowerDisplay.Lib\PowerDisplay.Lib.csproj" /> <ProjectReference Include="..\PowerDisplay.Lib\PowerDisplay.Lib.csproj" />

View File

@@ -6,6 +6,7 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -25,8 +26,10 @@ namespace PowerDisplay
public partial class App : Application public partial class App : Application
#pragma warning restore CA1001 #pragma warning restore CA1001
{ {
private readonly ISettingsUtils _settingsUtils = new SettingsUtils();
private Window? _mainWindow; private Window? _mainWindow;
private int _powerToysRunnerPid; private int _powerToysRunnerPid;
private TrayIconService? _trayIconService;
public App(int runnerPid) public App(int runnerPid)
{ {
@@ -90,7 +93,16 @@ namespace PowerDisplay
RegisterWindowEvent(Constants.ShowPowerDisplayEvent(), mw => mw.ShowWindow(), "Show"); RegisterWindowEvent(Constants.ShowPowerDisplayEvent(), mw => mw.ShowWindow(), "Show");
RegisterWindowEvent(Constants.TogglePowerDisplayEvent(), mw => mw.ToggleWindow(), "Toggle"); RegisterWindowEvent(Constants.TogglePowerDisplayEvent(), mw => mw.ToggleWindow(), "Toggle");
RegisterEvent(Constants.TerminatePowerDisplayEvent(), () => Environment.Exit(0), "Terminate"); RegisterEvent(Constants.TerminatePowerDisplayEvent(), () => Environment.Exit(0), "Terminate");
RegisterViewModelEvent(Constants.SettingsUpdatedPowerDisplayEvent(), vm => vm.ApplySettingsFromUI(), "SettingsUpdated"); RegisterViewModelEvent(
Constants.SettingsUpdatedPowerDisplayEvent(),
vm =>
{
vm.ApplySettingsFromUI();
// Refresh tray icon based on updated settings
_trayIconService?.SetupTrayIcon();
},
"SettingsUpdated");
RegisterViewModelEvent(Constants.ApplyColorTemperaturePowerDisplayEvent(), vm => vm.ApplyColorTemperatureFromSettings(), "ApplyColorTemperature"); RegisterViewModelEvent(Constants.ApplyColorTemperaturePowerDisplayEvent(), vm => vm.ApplyColorTemperatureFromSettings(), "ApplyColorTemperature");
RegisterViewModelEvent(Constants.ApplyProfilePowerDisplayEvent(), vm => vm.ApplyProfileFromSettings(), "ApplyProfile"); RegisterViewModelEvent(Constants.ApplyProfilePowerDisplayEvent(), vm => vm.ApplyProfileFromSettings(), "ApplyProfile");
@@ -113,6 +125,15 @@ namespace PowerDisplay
// Create main window // Create main window
_mainWindow = new MainWindow(); _mainWindow = new MainWindow();
// Initialize tray icon service
_trayIconService = new TrayIconService(
_settingsUtils,
ShowMainWindow,
ToggleMainWindow,
() => Environment.Exit(0),
OpenSettings);
_trayIconService.SetupTrayIcon();
// Window visibility depends on launch mode // Window visibility depends on launch mode
bool isStandaloneMode = _powerToysRunnerPid <= 0; bool isStandaloneMode = _powerToysRunnerPid <= 0;
@@ -276,6 +297,44 @@ namespace PowerDisplay
/// </summary> /// </summary>
public Window? MainWindow => _mainWindow; public Window? MainWindow => _mainWindow;
/// <summary>
/// Show the main window
/// </summary>
private void ShowMainWindow()
{
if (_mainWindow is MainWindow mainWindow)
{
mainWindow.ShowWindow();
}
}
/// <summary>
/// Toggle the main window visibility
/// </summary>
private void ToggleMainWindow()
{
if (_mainWindow is MainWindow mainWindow)
{
mainWindow.ToggleWindow();
}
}
/// <summary>
/// Open PowerDisplay settings in PowerToys Settings UI
/// </summary>
private void OpenSettings()
{
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerDisplay, false);
}
/// <summary>
/// Refresh tray icon based on current settings
/// </summary>
public void RefreshTrayIcon()
{
_trayIconService?.SetupTrayIcon();
}
/// <summary> /// <summary>
/// Check if running standalone (not launched from PowerToys Runner) /// Check if running standalone (not launched from PowerToys Runner)
/// </summary> /// </summary>
@@ -290,6 +349,7 @@ namespace PowerDisplay
public void Shutdown() public void Shutdown()
{ {
Logger.LogInfo("PowerDisplay shutting down"); Logger.LogInfo("PowerDisplay shutting down");
_trayIconService?.Destroy();
Environment.Exit(0); Environment.Exit(0);
} }
} }

View File

@@ -54,4 +54,13 @@
<data name="BrightnessAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="BrightnessAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Brightness</value> <value>Brightness</value>
</data> </data>
<data name="AppName" xml:space="preserve">
<value>PowerDisplay</value>
</data>
<data name="TrayMenu_Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="TrayMenu_Exit" xml:space="preserve">
<value>Exit</value>
</data>
</root> </root>

View File

@@ -20,6 +20,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
BrightnessUpdateRate = "1s"; BrightnessUpdateRate = "1s";
Monitors = new List<MonitorInfo>(); Monitors = new List<MonitorInfo>();
RestoreSettingsOnStartup = true; RestoreSettingsOnStartup = true;
ShowSystemTrayIcon = true;
// Note: saved_monitor_settings has been moved to monitor_state.json // Note: saved_monitor_settings has been moved to monitor_state.json
// which is managed separately by PowerDisplay app // which is managed separately by PowerDisplay app
@@ -37,6 +38,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("restore_settings_on_startup")] [JsonPropertyName("restore_settings_on_startup")]
public bool RestoreSettingsOnStartup { get; set; } public bool RestoreSettingsOnStartup { get; set; }
[JsonPropertyName("show_system_tray_icon")]
public bool ShowSystemTrayIcon { get; set; }
/// <summary> /// <summary>
/// Pending color temperature operation from Settings UI. /// Pending color temperature operation from Settings UI.
/// This is cleared after PowerDisplay processes it. /// This is cleared after PowerDisplay processes it.

View File

@@ -36,6 +36,9 @@
<tkcontrols:SettingsCard x:Uid="PowerDisplay_RestoreSettingsOnStartup" HeaderIcon="{ui:FontIcon Glyph=&#xE7B8;}"> <tkcontrols:SettingsCard x:Uid="PowerDisplay_RestoreSettingsOnStartup" HeaderIcon="{ui:FontIcon Glyph=&#xE7B8;}">
<ToggleSwitch x:Uid="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch" IsOn="{x:Bind ViewModel.RestoreSettingsOnStartup, Mode=TwoWay}" /> <ToggleSwitch x:Uid="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch" IsOn="{x:Bind ViewModel.RestoreSettingsOnStartup, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ShowSystemTrayIcon" HeaderIcon="{ui:FontIcon Glyph=&#xE75B;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_BrightnessUpdateRate" HeaderIcon="{ui:FontIcon Glyph=&#xE916;}"> <tkcontrols:SettingsCard x:Uid="PowerDisplay_BrightnessUpdateRate" HeaderIcon="{ui:FontIcon Glyph=&#xE916;}">
<ComboBox <ComboBox

View File

@@ -5583,6 +5583,12 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch.OffContent" xml:space="preserve"> <data name="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value> <value>Off</value>
</data> </data>
<data name="PowerDisplay_ShowSystemTrayIcon.Header" xml:space="preserve">
<value>Show system tray icon</value>
</data>
<data name="PowerDisplay_ShowSystemTrayIcon.Description" xml:space="preserve">
<value>Choose if PowerDisplay is visible in the system tray</value>
</data>
<data name="PowerDisplay_BrightnessUpdateRate.Header" xml:space="preserve"> <data name="PowerDisplay_BrightnessUpdateRate.Header" xml:space="preserve">
<value>Brightness update rate</value> <value>Brightness update rate</value>
</data> </data>

View File

@@ -109,6 +109,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
set => SetSettingsProperty(_settings.Properties.RestoreSettingsOnStartup, value, v => _settings.Properties.RestoreSettingsOnStartup = v); set => SetSettingsProperty(_settings.Properties.RestoreSettingsOnStartup, value, v => _settings.Properties.RestoreSettingsOnStartup = v);
} }
public bool ShowSystemTrayIcon
{
get => _settings.Properties.ShowSystemTrayIcon;
set
{
if (SetSettingsProperty(_settings.Properties.ShowSystemTrayIcon, value, v => _settings.Properties.ShowSystemTrayIcon = v))
{
// Explicitly signal PowerDisplay to refresh tray icon
// This is needed because set_config() doesn't signal SettingsUpdatedEvent to avoid UI refresh issues
using var eventHandle = new System.Threading.EventWaitHandle(
false,
System.Threading.EventResetMode.AutoReset,
Constants.SettingsUpdatedPowerDisplayEvent());
eventHandle.Set();
Logger.LogInfo($"ShowSystemTrayIcon changed to {value}, signaled SettingsUpdatedPowerDisplayEvent");
}
}
}
public HotkeySettings ActivationShortcut public HotkeySettings ActivationShortcut
{ {
get => _settings.Properties.ActivationShortcut; get => _settings.Properties.ActivationShortcut;