mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 03:37:59 +01:00
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:
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 283 B |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconService.cs
Normal file
276
src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/modules/powerdisplay/PowerDisplay/NativeMethods.json
Normal file
5
src/modules/powerdisplay/PowerDisplay/NativeMethods.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||||
|
"public": true,
|
||||||
|
"allowMarshaling": false
|
||||||
|
}
|
||||||
19
src/modules/powerdisplay/PowerDisplay/NativeMethods.txt
Normal file
19
src/modules/powerdisplay/PowerDisplay/NativeMethods.txt
Normal 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
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_RestoreSettingsOnStartup" HeaderIcon="{ui:FontIcon Glyph=}">
|
<tkcontrols:SettingsCard x:Uid="PowerDisplay_RestoreSettingsOnStartup" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
<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=}">
|
||||||
|
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
|
|
||||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_BrightnessUpdateRate" HeaderIcon="{ui:FontIcon Glyph=}">
|
<tkcontrols:SettingsCard x:Uid="PowerDisplay_BrightnessUpdateRate" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
<ComboBox
|
<ComboBox
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user