tray_icon.cpp
1 #include "pch.h" 2 #include "Generated files/resource.h" 3 #include "settings_window.h" 4 #include "tray_icon.h" 5 #include "general_settings.h" 6 #include "centralized_hotkeys.h" 7 #include "centralized_kb_hook.h" 8 #include "quick_access_host.h" 9 #include "hotkey_conflict_detector.h" 10 #include "trace.h" 11 #include <Windows.h> 12 13 #include <common/utils/resources.h> 14 #include <common/version/version.h> 15 #include <common/logger/logger.h> 16 #include <common/utils/elevation.h> 17 #include <common/Themes/theme_listener.h> 18 #include <common/Themes/theme_helpers.h> 19 #include "bug_report.h" 20 21 namespace 22 { 23 HWND tray_icon_hwnd = NULL; 24 25 enum 26 { 27 wm_icon_notify = WM_APP, 28 wm_run_on_main_ui_thread, 29 }; 30 31 // Contains the Windows Message for taskbar creation. 32 UINT wm_taskbar_restart = 0; 33 34 NOTIFYICONDATAW tray_icon_data; 35 bool tray_icon_created = false; 36 37 bool about_box_shown = false; 38 39 HMENU h_menu = nullptr; 40 HMENU h_sub_menu = nullptr; 41 bool double_click_timer_running = false; 42 bool double_clicked = false; 43 POINT tray_icon_click_point; 44 std::optional<bool> last_quick_access_state; // Track the last known Quick Access state 45 46 static ThemeListener theme_listener; 47 static bool theme_adaptive_enabled = false; 48 } 49 50 // Struct to fill with callback and the data. The window_proc is responsible for cleaning it. 51 struct run_on_main_ui_thread_msg 52 { 53 main_loop_callback_function _callback; 54 PVOID data; 55 }; 56 57 bool dispatch_run_on_main_ui_thread(main_loop_callback_function _callback, PVOID data) 58 { 59 if (tray_icon_hwnd == NULL) 60 { 61 return false; 62 } 63 struct run_on_main_ui_thread_msg* wnd_msg = new struct run_on_main_ui_thread_msg(); 64 wnd_msg->_callback = _callback; 65 wnd_msg->data = data; 66 67 PostMessage(tray_icon_hwnd, wm_run_on_main_ui_thread, 0, reinterpret_cast<LPARAM>(wnd_msg)); 68 69 return true; 70 } 71 72 void change_menu_item_text(const UINT item_id, wchar_t* new_text) 73 { 74 MENUITEMINFOW menuitem = { .cbSize = sizeof(MENUITEMINFOW), .fMask = MIIM_TYPE | MIIM_DATA }; 75 GetMenuItemInfoW(h_menu, item_id, false, &menuitem); 76 menuitem.dwTypeData = new_text; 77 SetMenuItemInfoW(h_menu, item_id, false, &menuitem); 78 } 79 80 void open_quick_access_flyout_window() 81 { 82 QuickAccessHost::show(); 83 } 84 85 void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam) 86 { 87 switch (command_id) 88 { 89 case ID_SETTINGS_MENU_COMMAND: 90 { 91 std::wstring settings_window{ winrt::to_hstring(ESettingsWindowNames_to_string(static_cast<ESettingsWindowNames>(lparam))) }; 92 open_settings_window(settings_window); 93 } 94 break; 95 case ID_CLOSE_MENU_COMMAND: 96 if (h_menu) 97 { 98 DestroyMenu(h_menu); 99 } 100 DestroyWindow(window); 101 break; 102 case ID_ABOUT_MENU_COMMAND: 103 if (!about_box_shown) 104 { 105 about_box_shown = true; 106 std::wstring about_msg = L"PowerToys\nVersion " + get_product_version() + L"\n\xa9 2019 Microsoft Corporation"; 107 MessageBoxW(nullptr, about_msg.c_str(), L"About PowerToys", MB_OK); 108 about_box_shown = false; 109 } 110 break; 111 case ID_REPORT_BUG_COMMAND: 112 { 113 launch_bug_report(); 114 break; 115 } 116 117 case ID_DOCUMENTATION_MENU_COMMAND: 118 { 119 RunNonElevatedEx(L"https://aka.ms/PowerToysOverview", L"", L""); 120 break; 121 } 122 case ID_QUICK_ACCESS_MENU_COMMAND: 123 { 124 open_quick_access_flyout_window(); 125 break; 126 } 127 } 128 } 129 130 void click_timer_elapsed() 131 { 132 double_click_timer_running = false; 133 if (!double_clicked) 134 { 135 // Log telemetry for single click (confirmed it's not a double click) 136 Trace::TrayIconLeftClick(get_general_settings().enableQuickAccess); 137 138 if (get_general_settings().enableQuickAccess) 139 { 140 open_quick_access_flyout_window(); 141 } 142 else 143 { 144 open_settings_window(std::nullopt); 145 } 146 } 147 } 148 149 LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) 150 { 151 switch (message) 152 { 153 case WM_HOTKEY: 154 { 155 // We use the tray icon WndProc to avoid creating a dedicated window just for this message. 156 const auto modifiersMask = LOWORD(lparam); 157 const auto vkCode = HIWORD(lparam); 158 Logger::trace(L"On {} hotkey", CentralizedHotkeys::ToWstring({ modifiersMask, vkCode })); 159 CentralizedHotkeys::PopulateHotkey({ modifiersMask, vkCode }); 160 break; 161 } 162 case WM_CREATE: 163 if (wm_taskbar_restart == 0) 164 { 165 tray_icon_hwnd = window; 166 wm_taskbar_restart = RegisterWindowMessageW(L"TaskbarCreated"); 167 } 168 break; 169 case WM_DESTROY: 170 if (tray_icon_created) 171 { 172 Shell_NotifyIcon(NIM_DELETE, &tray_icon_data); 173 tray_icon_created = false; 174 } 175 close_settings_window(); 176 PostQuitMessage(0); 177 break; 178 case WM_CLOSE: 179 DestroyWindow(window); 180 break; 181 case WM_COMMAND: 182 handle_tray_command(window, wparam, lparam); 183 break; 184 // Shell_NotifyIcon can fail when we invoke it during the time explorer.exe isn't present/ready to handle it. 185 // We'll also never receive wm_taskbar_restart message if the first call to Shell_NotifyIcon failed, so we use 186 // WM_WINDOWPOSCHANGING which is always received on explorer startup sequence. 187 case WM_WINDOWPOSCHANGING: 188 { 189 if (!tray_icon_created) 190 { 191 tray_icon_created = Shell_NotifyIcon(NIM_ADD, &tray_icon_data) == TRUE; 192 } 193 break; 194 } 195 default: 196 if (message == wm_icon_notify) 197 { 198 switch (lparam) 199 { 200 case WM_RBUTTONUP: 201 case WM_CONTEXTMENU: 202 { 203 bool quick_access_enabled = get_general_settings().enableQuickAccess; 204 205 // Log telemetry 206 Trace::TrayIconRightClick(quick_access_enabled); 207 208 // Reload menu if Quick Access state has changed or is first time 209 if (h_menu && (!last_quick_access_state.has_value() || quick_access_enabled != last_quick_access_state.value())) 210 { 211 DestroyMenu(h_menu); 212 h_menu = nullptr; 213 h_sub_menu = nullptr; 214 } 215 216 last_quick_access_state = quick_access_enabled; 217 218 if (!h_menu) 219 { 220 h_menu = LoadMenu(reinterpret_cast<HINSTANCE>(&__ImageBase), MAKEINTRESOURCE(ID_TRAY_MENU)); 221 } 222 if (h_menu) 223 { 224 static std::wstring settings_menuitem_label = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT); 225 static std::wstring settings_menuitem_label_leftclick = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT_LEFTCLICK); 226 static std::wstring close_menuitem_label = GET_RESOURCE_STRING(IDS_CLOSE_MENU_TEXT); 227 static std::wstring submit_bug_menuitem_label = GET_RESOURCE_STRING(IDS_SUBMIT_BUG_TEXT); 228 static std::wstring documentation_menuitem_label = GET_RESOURCE_STRING(IDS_DOCUMENTATION_MENU_TEXT); 229 static std::wstring quick_access_menuitem_label = GET_RESOURCE_STRING(IDS_QUICK_ACCESS_MENU_TEXT); 230 231 // Update Settings menu text based on Quick Access state 232 if (quick_access_enabled) 233 { 234 change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data()); 235 } 236 else 237 { 238 change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label_leftclick.data()); 239 } 240 241 change_menu_item_text(ID_CLOSE_MENU_COMMAND, close_menuitem_label.data()); 242 change_menu_item_text(ID_REPORT_BUG_COMMAND, submit_bug_menuitem_label.data()); 243 bool bug_report_disabled = is_bug_report_running(); 244 EnableMenuItem(h_sub_menu, ID_REPORT_BUG_COMMAND, MF_BYCOMMAND | (bug_report_disabled ? MF_GRAYED : MF_ENABLED)); 245 change_menu_item_text(ID_DOCUMENTATION_MENU_COMMAND, documentation_menuitem_label.data()); 246 change_menu_item_text(ID_QUICK_ACCESS_MENU_COMMAND, quick_access_menuitem_label.data()); 247 248 // Hide or show Quick Access menu item based on setting 249 if (!h_sub_menu) 250 { 251 h_sub_menu = GetSubMenu(h_menu, 0); 252 } 253 if (!quick_access_enabled) 254 { 255 // Remove Quick Access menu item when disabled 256 DeleteMenu(h_sub_menu, ID_QUICK_ACCESS_MENU_COMMAND, MF_BYCOMMAND); 257 } 258 } 259 if (!h_sub_menu) 260 { 261 h_sub_menu = GetSubMenu(h_menu, 0); 262 } 263 POINT mouse_pointer; 264 GetCursorPos(&mouse_pointer); 265 SetForegroundWindow(window); // Needed for the context menu to disappear. 266 TrackPopupMenu(h_sub_menu, TPM_CENTERALIGN | TPM_BOTTOMALIGN, mouse_pointer.x, mouse_pointer.y, 0, window, nullptr); 267 break; 268 } 269 case WM_LBUTTONUP: 270 { 271 // ignore event if this is the second click of a double click 272 if (!double_click_timer_running) 273 { 274 // start timer for detecting single or double click 275 double_click_timer_running = true; 276 double_clicked = false; 277 278 UINT doubleClickTime = GetDoubleClickTime(); 279 std::thread([doubleClickTime]() { 280 std::this_thread::sleep_for(std::chrono::milliseconds(doubleClickTime)); 281 click_timer_elapsed(); 282 }).detach(); 283 } 284 break; 285 } 286 case WM_LBUTTONDBLCLK: 287 { 288 // Log telemetry 289 Trace::TrayIconDoubleClick(get_general_settings().enableQuickAccess); 290 291 double_clicked = true; 292 open_settings_window(std::nullopt); 293 break; 294 } 295 break; 296 } 297 } 298 else if (message == wm_run_on_main_ui_thread) 299 { 300 if (lparam != NULL) 301 { 302 struct run_on_main_ui_thread_msg* msg = reinterpret_cast<struct run_on_main_ui_thread_msg*>(lparam); 303 msg->_callback(msg->data); 304 delete msg; 305 lparam = NULL; 306 } 307 break; 308 } 309 else if (message == wm_taskbar_restart) 310 { 311 tray_icon_created = Shell_NotifyIcon(NIM_ADD, &tray_icon_data) == TRUE; 312 break; 313 } 314 } 315 return DefWindowProc(window, message, wparam, lparam); 316 } 317 318 static HICON get_icon(Theme theme) 319 { 320 std::wstring icon_path = get_module_folderpath(); 321 icon_path += theme == Theme::Dark ? L"\\svgs\\PowerToysWhite.ico" : L"\\svgs\\PowerToysDark.ico"; 322 Logger::trace(L"get_icon: Loading icon from path: {}", icon_path); 323 324 HICON icon = static_cast<HICON>(LoadImage(NULL, 325 icon_path.c_str(), 326 IMAGE_ICON, 327 0, 328 0, 329 LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED)); 330 if (!icon) 331 { 332 Logger::warn(L"get_icon: Failed to load icon from {}, error: {}", icon_path, GetLastError()); 333 } 334 return icon; 335 } 336 337 338 static void handle_theme_change() 339 { 340 if (theme_adaptive_enabled) 341 { 342 tray_icon_data.hIcon = get_icon(ThemeHelpers::GetSystemTheme()); 343 Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data); 344 } 345 } 346 347 void update_bug_report_menu_status(bool isRunning) 348 { 349 if (h_sub_menu != nullptr) 350 { 351 EnableMenuItem(h_sub_menu, ID_REPORT_BUG_COMMAND, MF_BYCOMMAND | (isRunning ? MF_GRAYED : MF_ENABLED)); 352 } 353 } 354 355 void start_tray_icon(bool isProcessElevated, bool theme_adaptive) 356 { 357 theme_adaptive_enabled = theme_adaptive; 358 auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase); 359 HICON const icon = theme_adaptive ? get_icon(ThemeHelpers::GetSystemTheme()) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON)); 360 if (icon) 361 { 362 UINT id_tray_icon = 1; 363 364 WNDCLASS wc = {}; 365 wc.hCursor = LoadCursor(nullptr, IDC_ARROW); 366 wc.hInstance = h_instance; 367 wc.lpszClassName = pt_tray_icon_window_class; 368 wc.style = CS_HREDRAW | CS_VREDRAW; 369 wc.lpfnWndProc = tray_icon_window_proc; 370 wc.hIcon = icon; 371 RegisterClass(&wc); 372 auto hwnd = CreateWindowW(wc.lpszClassName, 373 pt_tray_icon_window_class, 374 WS_OVERLAPPEDWINDOW | WS_POPUP, 375 CW_USEDEFAULT, 376 CW_USEDEFAULT, 377 CW_USEDEFAULT, 378 CW_USEDEFAULT, 379 nullptr, 380 nullptr, 381 wc.hInstance, 382 nullptr); 383 WINRT_VERIFY(hwnd); 384 CentralizedHotkeys::RegisterWindow(hwnd); 385 CentralizedKeyboardHook::RegisterWindow(hwnd); 386 memset(&tray_icon_data, 0, sizeof(tray_icon_data)); 387 tray_icon_data.cbSize = sizeof(tray_icon_data); 388 tray_icon_data.hIcon = icon; 389 tray_icon_data.hWnd = hwnd; 390 tray_icon_data.uID = id_tray_icon; 391 tray_icon_data.uCallbackMessage = wm_icon_notify; 392 393 std::wstringstream pt_version_tooltip_stream; 394 if (isProcessElevated) 395 { 396 pt_version_tooltip_stream << GET_RESOURCE_STRING(IDS_TRAY_ICON_ADMIN_TOOLTIP) << L": "; 397 } 398 399 pt_version_tooltip_stream << L"PowerToys " << get_product_version() << '\0'; 400 std::wstring pt_version_tooltip = pt_version_tooltip_stream.str(); 401 wcscpy_s(tray_icon_data.szTip, sizeof(tray_icon_data.szTip) / sizeof(WCHAR), pt_version_tooltip.c_str()); 402 tray_icon_data.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE; 403 ChangeWindowMessageFilterEx(hwnd, WM_COMMAND, MSGFLT_ALLOW, nullptr); 404 405 tray_icon_created = Shell_NotifyIcon(NIM_ADD, &tray_icon_data) == TRUE; 406 theme_listener.AddSystemThemeChangedHandler(&handle_theme_change); 407 408 // Register callback to update bug report menu item status 409 BugReportManager::instance().register_callback([](bool isRunning) { 410 dispatch_run_on_main_ui_thread([](PVOID data) { 411 bool* running = static_cast<bool*>(data); 412 update_bug_report_menu_status(*running); 413 delete running; 414 }, 415 new bool(isRunning)); 416 }); 417 } 418 } 419 420 void set_tray_icon_visible(bool shouldIconBeVisible) 421 { 422 tray_icon_data.uFlags |= NIF_STATE; 423 tray_icon_data.dwStateMask = NIS_HIDDEN; 424 tray_icon_data.dwState = shouldIconBeVisible ? 0 : NIS_HIDDEN; 425 Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data); 426 } 427 428 void set_tray_icon_theme_adaptive(bool theme_adaptive) 429 { 430 Logger::info(L"set_tray_icon_theme_adaptive: Called with theme_adaptive={}, current theme_adaptive_enabled={}", 431 theme_adaptive, theme_adaptive_enabled); 432 433 auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase); 434 HICON icon = nullptr; 435 436 if (theme_adaptive) 437 { 438 icon = get_icon(ThemeHelpers::GetSystemTheme()); 439 if (!icon) 440 { 441 Logger::warn(L"set_tray_icon_theme_adaptive: Failed to load theme adaptive icon, falling back to default"); 442 } 443 } 444 445 // If not requesting adaptive icon, or if adaptive icon failed to load, use default icon 446 if (!icon) 447 { 448 icon = LoadIcon(h_instance, MAKEINTRESOURCE(APPICON)); 449 if (theme_adaptive && icon) 450 { 451 // We requested adaptive but had to fall back, so update the flag 452 theme_adaptive = false; 453 Logger::info(L"set_tray_icon_theme_adaptive: Using default icon as fallback"); 454 } 455 } 456 457 theme_adaptive_enabled = theme_adaptive; 458 459 if (icon) 460 { 461 tray_icon_data.hIcon = icon; 462 BOOL result = Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data); 463 Logger::info(L"set_tray_icon_theme_adaptive: Icon updated, theme_adaptive_enabled={}, Shell_NotifyIcon result={}", 464 theme_adaptive_enabled, result); 465 } 466 else 467 { 468 Logger::error(L"set_tray_icon_theme_adaptive: Failed to load any icon"); 469 } 470 } 471 472 void stop_tray_icon() 473 { 474 if (tray_icon_created) 475 { 476 // Clear bug report callbacks 477 BugReportManager::instance().clear_callbacks(); 478 SendMessage(tray_icon_hwnd, WM_CLOSE, 0, 0); 479 } 480 } 481 void update_quick_access_hotkey(bool enabled, PowerToysSettings::HotkeyObject hotkey) 482 { 483 static PowerToysSettings::HotkeyObject current_hotkey; 484 static bool is_registered = false; 485 auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); 486 487 if (is_registered) 488 { 489 CentralizedKeyboardHook::ClearModuleHotkeys(L"QuickAccess"); 490 hkmng.RemoveHotkeyByModule(L"GeneralSettings"); 491 is_registered = false; 492 } 493 494 if (enabled && hotkey.get_code() != 0) 495 { 496 HotkeyConflictDetector::Hotkey hk = { 497 hotkey.win_pressed(), 498 hotkey.ctrl_pressed(), 499 hotkey.shift_pressed(), 500 hotkey.alt_pressed(), 501 static_cast<unsigned char>(hotkey.get_code()) 502 }; 503 504 hkmng.AddHotkey(hk, L"GeneralSettings", 0, true); 505 CentralizedKeyboardHook::SetHotkeyAction(L"QuickAccess", hk, []() { 506 open_quick_access_flyout_window(); 507 return true; 508 }); 509 510 current_hotkey = hotkey; 511 is_registered = true; 512 } 513 }