mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-04 02:06:36 +02:00
<!-- 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 Based on https://github.com/microsoft/PowerToys/pull/40260#issuecomment-3085099815 feedback, this PR remove the title bar shutdown button in favor of a menu item in the navigation view footer. - Menu item is visible only when tray icon is hidden - A confirm dialog has been added <img width="848" height="448" alt="image" src="https://github.com/user-attachments/assets/529bcfa9-94ed-48b1-b2bb-ca6993d12e0f" /> <img width="848" height="448" alt="image" src="https://github.com/user-attachments/assets/febafbb4-3a5b-4b04-8065-28f0d269ab6c" /> - Close is used in tray icon menu for closing app <img alt="image" src="https://github.com/user-attachments/assets/3ac79a8c-961f-4f95-8967-adef00aba77b" /> <img alt="image" src="https://github.com/user-attachments/assets/c2800a77-c733-41a9-aa4f-fa4c2afd30a3" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] **Closes:** #40346 #40577 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed - Open settings with tray icon visible: close menu is hidden - Open settings with tray icon hidden: close menu is visible - Tested close menu visibility change when tray icon option is changed - Tested cancel button of close dialog - Tested close button of dialog --------- Co-authored-by: Niels Laute <niels.laute@live.nl>
214 lines
8.1 KiB
C#
214 lines
8.1 KiB
C#
// 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.CodeAnalysis;
|
|
using System.Runtime.InteropServices;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using Microsoft.CmdPal.UI.Messages;
|
|
using Microsoft.CmdPal.UI.ViewModels;
|
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
|
using Microsoft.UI.Xaml;
|
|
using Windows.Win32;
|
|
using Windows.Win32.Foundation;
|
|
using Windows.Win32.UI.Shell;
|
|
using Windows.Win32.UI.WindowsAndMessaging;
|
|
using WinRT.Interop;
|
|
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
|
|
|
namespace Microsoft.CmdPal.UI.Helpers;
|
|
|
|
[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 = 1000;
|
|
private const uint WM_TRAY_ICON = PInvoke.WM_USER + 1;
|
|
|
|
private readonly SettingsModel _settingsModel;
|
|
private readonly uint WM_TASKBAR_RESTART;
|
|
|
|
private Window? _window;
|
|
private HWND _hwnd;
|
|
private WNDPROC? _originalWndProc;
|
|
private WNDPROC? _trayWndProc;
|
|
private NOTIFYICONDATAW? _trayIconData;
|
|
private DestroyIconSafeHandle? _largeIcon;
|
|
private DestroyMenuSafeHandle? _popupMenu;
|
|
|
|
public TrayIconService(SettingsModel settingsModel)
|
|
{
|
|
_settingsModel = settingsModel;
|
|
|
|
// 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)
|
|
{
|
|
if (showSystemTrayIcon ?? _settingsModel.ShowSystemTrayIcon)
|
|
{
|
|
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 = Marshal.GetDelegateForFunctionPointer<WNDPROC>(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();
|
|
_trayIconData = new NOTIFYICONDATAW()
|
|
{
|
|
cbSize = (uint)Marshal.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 = RS_.GetString("AppStoreName"),
|
|
};
|
|
}
|
|
|
|
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, RS_.GetString("TrayMenu_Settings"));
|
|
PInvoke.InsertMenu(_popupMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 2, RS_.GetString("TrayMenu_Close"));
|
|
}
|
|
}
|
|
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 DestroyIconSafeHandle GetAppIconHandle()
|
|
{
|
|
var exePath = Path.Combine(AppContext.BaseDirectory, "Microsoft.CmdPal.UI.exe");
|
|
DestroyIconSafeHandle largeIcon;
|
|
PInvoke.ExtractIconEx(exePath, 0, out 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)
|
|
{
|
|
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
|
}
|
|
else if (wParam == PInvoke.WM_USER + 2)
|
|
{
|
|
WeakReferenceMessenger.Default.Send<QuitMessage>();
|
|
}
|
|
}
|
|
|
|
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
|
|
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:
|
|
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(string.Empty, HWND.Null));
|
|
break;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
|
|
}
|
|
}
|