/ src / runner / tray_icon.cpp
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  }