2025-06-05 22:29:18 +02:00
// 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 ;
2025-08-13 13:42:52 -05:00
using Microsoft.CmdPal.UI.Messages ;
2025-06-05 22:29:18 +02:00
using Microsoft.CmdPal.UI.ViewModels ;
2025-08-13 13:42:52 -05:00
using Microsoft.CmdPal.UI.ViewModels.Messages ;
2025-06-05 22:29:18 +02:00
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 )
{
2025-08-18 06:07:28 -05:00
if ( _window is null )
2025-06-05 22:29:18 +02:00
{
_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 ) ) ;
}
2025-08-18 06:07:28 -05:00
if ( _trayIconData is null )
2025-06-05 22:29:18 +02:00
{
// 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 ( )
{
2025-07-01 10:36:59 +08:00
cbSize = ( uint ) Marshal . SizeOf < NOTIFYICONDATAW > ( ) ,
2025-06-05 22:29:18 +02:00
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 ) ;
2025-08-18 06:07:28 -05:00
if ( _popupMenu is null )
2025-06-05 22:29:18 +02:00
{
_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" ) ) ;
2025-08-21 10:40:37 +02:00
PInvoke . InsertMenu ( _popupMenu , 1 , MENU_ITEM_FLAGS . MF_BYPOSITION | MENU_ITEM_FLAGS . MF_STRING , PInvoke . WM_USER + 2 , RS_ . GetString ( "TrayMenu_Close" ) ) ;
2025-06-05 22:29:18 +02:00
}
}
else
{
Destroy ( ) ;
}
}
public void Destroy ( )
{
2025-08-18 06:07:28 -05:00
if ( _trayIconData is not null )
2025-06-05 22:29:18 +02:00
{
var d = ( NOTIFYICONDATAW ) _trayIconData ;
if ( PInvoke . Shell_NotifyIcon ( NOTIFY_ICON_MESSAGE . NIM_DELETE , in d ) )
{
_trayIconData = null ;
}
}
2025-08-18 06:07:28 -05:00
if ( _popupMenu is not null )
2025-06-05 22:29:18 +02:00
{
_popupMenu . Close ( ) ;
_popupMenu = null ;
}
2025-08-18 06:07:28 -05:00
if ( _largeIcon is not null )
2025-06-05 22:29:18 +02:00
{
_largeIcon . Close ( ) ;
_largeIcon = null ;
}
2025-08-18 06:07:28 -05:00
if ( _window is not null )
2025-06-05 22:29:18 +02:00
{
_window . Close ( ) ;
_window = null ;
_hwnd = HWND . Null ;
}
}
private DestroyIconSafeHandle GetAppIconHandle ( )
{
2025-07-01 10:36:59 +08:00
var exePath = Path . Combine ( AppContext . BaseDirectory , "Microsoft.CmdPal.UI.exe" ) ;
2025-06-05 22:29:18 +02:00
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 :
{
2025-08-18 06:07:28 -05:00
if ( _trayIconData is null )
2025-06-05 22:29:18 +02:00
{
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 :
{
2025-08-18 06:07:28 -05:00
if ( _popupMenu is not null )
2025-06-05 22:29:18 +02:00
{
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 ) ;
}
}