/ src / modules / ShortcutGuide / ShortcutGuide / shortcut_guide.cpp
shortcut_guide.cpp
  1  #include "pch.h"
  2  #include "shortcut_guide.h"
  3  #include "target_state.h"
  4  #include "trace.h"
  5  
  6  #include <common/SettingsAPI/settings_objects.h>
  7  #include <common/debug_control.h>
  8  #include <common/interop/shared_constants.h>
  9  #include <sstream>
 10  
 11  #include <common/SettingsAPI/settings_helpers.h>
 12  #include <common/SettingsAPI/settings_objects.h>
 13  #include <common/logger/logger.h>
 14  #include <common/utils/process_path.h>
 15  #include <common/utils/resources.h>
 16  #include <common/utils/string_utils.h>
 17  #include <common/utils/winapi_error.h>
 18  #include <common/utils/window.h>
 19  #include <Psapi.h>
 20  #include <common/hooks/LowlevelKeyboardEvent.h>
 21  
 22  // TODO: refactor singleton
 23  OverlayWindow* overlay_window_instance = nullptr;
 24  
 25  namespace
 26  {
 27      // Window properties relevant to ShortcutGuide
 28      struct ShortcutGuideWindowInfo
 29      {
 30          HWND hwnd = nullptr; // Handle to the top-level foreground window or nullptr if there is no such window
 31          bool snappable = false; // True, if the window can react to Windows Snap keys
 32          bool disabled = false;
 33      };
 34  
 35      ShortcutGuideWindowInfo GetShortcutGuideWindowInfo(HWND active_window)
 36      {
 37          ShortcutGuideWindowInfo result;
 38          active_window = GetAncestor(active_window, GA_ROOT);
 39          if (!IsWindowVisible(active_window))
 40          {
 41              return result;
 42          }
 43  
 44          auto style = GetWindowLong(active_window, GWL_STYLE);
 45          auto exStyle = GetWindowLong(active_window, GWL_EXSTYLE);
 46          if ((style & WS_CHILD) == WS_CHILD ||
 47              (style & WS_DISABLED) == WS_DISABLED ||
 48              (exStyle & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW ||
 49              (exStyle & WS_EX_NOACTIVATE) == WS_EX_NOACTIVATE)
 50          {
 51              return result;
 52          }
 53          std::array<char, 256> class_name;
 54          GetClassNameA(active_window, class_name.data(), static_cast<int>(class_name.size()));
 55          if (is_system_window(active_window, class_name.data()))
 56          {
 57              return result;
 58          }
 59          static HWND cortana_hwnd = nullptr;
 60          if (cortana_hwnd == nullptr)
 61          {
 62              if (strcmp(class_name.data(), "Windows.UI.Core.CoreWindow") == 0 &&
 63                  get_process_path(active_window).ends_with(L"SearchUI.exe"))
 64              {
 65                  cortana_hwnd = active_window;
 66                  return result;
 67              }
 68          }
 69          else if (cortana_hwnd == active_window)
 70          {
 71              return result;
 72          }
 73          result.hwnd = active_window;
 74          // In reality, Windows Snap works if even one of those styles is set
 75          // for a window, it is just limited. If there is no WS_MAXIMIZEBOX using
 76          // WinKey + Up just won't maximize the window. Similarly, without
 77          // WS_MINIMIZEBOX the window will not get minimized. A "Save As..." dialog
 78          // is a example of such window - it can be snapped to both sides and to
 79          // all screen corners, but will not get maximized nor minimized.
 80          // For now, since ShortcutGuide can only disable entire "Windows Controls"
 81          // group, we require that the window supports all the options.
 82          result.snappable = ((style & WS_MAXIMIZEBOX) == WS_MAXIMIZEBOX) &&
 83                             ((style & WS_MINIMIZEBOX) == WS_MINIMIZEBOX) &&
 84                             ((style & WS_THICKFRAME) == WS_THICKFRAME);
 85          return result;
 86      }
 87  
 88      const LPARAM eventActivateWindow = 1;
 89  
 90      bool wasWinPressed = false;
 91      bool isWinPressed()
 92      {
 93          return (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000);
 94      }
 95  
 96      // all modifiers without win key
 97      std::vector<int> modifierKeys = { VK_SHIFT, VK_LSHIFT, VK_RSHIFT, VK_CONTROL, VK_LCONTROL, VK_RCONTROL, VK_MENU, VK_LMENU, VK_RMENU };
 98  
 99      // returns false if there are other modifiers pressed or win key isn' pressed
100      bool onlyWinPressed()
101      {
102          if (!isWinPressed())
103          {
104              return false;
105          }
106  
107          for (auto key : modifierKeys)
108          {
109              if (GetAsyncKeyState(key) & 0x8000)
110              {
111                  return false;
112              }
113          }
114  
115          return true;
116      }
117  
118      constexpr bool isWin(int key)
119      {
120          return key == VK_LWIN || key == VK_RWIN;
121      }
122  
123      constexpr bool isKeyDown(LowlevelKeyboardEvent event)
124      {
125          return event.wParam == WM_KEYDOWN || event.wParam == WM_SYSKEYDOWN;
126      }
127  
128      LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
129      {
130          LowlevelKeyboardEvent event;
131          if (nCode == HC_ACTION)
132          {
133              event.lParam = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
134              event.wParam = wParam;
135  
136              if (event.lParam->vkCode == VK_ESCAPE)
137              {
138                  Logger::trace(L"ESC key was pressed");
139                  overlay_window_instance->CloseWindow(HideWindowType::ESC_PRESSED);
140              }
141  
142              if (wasWinPressed && !isKeyDown(event) && isWin(event.lParam->vkCode))
143              {
144                  Logger::trace(L"Win key was released");
145                  overlay_window_instance->CloseWindow(HideWindowType::WIN_RELEASED);
146              }
147  
148              if (isKeyDown(event) && isWin(event.lParam->vkCode))
149              {
150                  wasWinPressed = true;
151              }
152  
153              if (onlyWinPressed() && isKeyDown(event) && !isWin(event.lParam->vkCode))
154              {
155                  Logger::trace(L"Shortcut with win key was pressed");
156                  overlay_window_instance->CloseWindow(HideWindowType::WIN_SHORTCUT_PRESSED);
157              }
158          }
159  
160          return CallNextHookEx(NULL, nCode, wParam, lParam);
161      }
162  
163      LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
164      {
165          if (nCode >= 0)
166          {
167              switch (wParam)
168              {
169              case WM_LBUTTONUP:
170              case WM_RBUTTONUP:
171              case WM_MBUTTONUP:
172              case WM_XBUTTONUP:
173                  // Don't close with mouse click if activation is windows key and the key is pressed
174                  if (!overlay_window_instance->win_key_activation() || !isWinPressed())
175                  {
176                      overlay_window_instance->CloseWindow(HideWindowType::MOUSE_BUTTONUP);
177                  }
178                  break;
179              default:
180                  break;
181              }
182          }
183  
184          return CallNextHookEx(0, nCode, wParam, lParam);
185      }
186  
187      std::wstring ToWstring(HideWindowType type)
188      {
189          switch (type)
190          {
191          case HideWindowType::ESC_PRESSED:
192              return L"ESC_PRESSED";
193          case HideWindowType::WIN_RELEASED:
194              return L"WIN_RELEASED";
195          case HideWindowType::WIN_SHORTCUT_PRESSED:
196              return L"WIN_SHORTCUT_PRESSED";
197          case HideWindowType::THE_SHORTCUT_PRESSED:
198              return L"THE_SHORTCUT_PRESSED";
199          case HideWindowType::MOUSE_BUTTONUP:
200              return L"MOUSE_BUTTONUP";
201          }
202  
203          return L"";
204      }
205  }
206  
207  OverlayWindow::OverlayWindow(HWND activeWindow)
208  {
209      overlay_window_instance = this;
210      this->activeWindow = activeWindow;
211      app_name = GET_RESOURCE_STRING(IDS_SHORTCUT_GUIDE);
212  
213      Logger::info("Overlay Window is creating");
214      init_settings();
215      keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(NULL), NULL);
216      if (!keyboardHook)
217      {
218          Logger::warn(L"Failed to create low level keyboard hook. {}", get_last_error_or_default(GetLastError()));
219      }
220  
221      mouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, GetModuleHandle(NULL), NULL);
222      if (!mouseHook)
223      {
224          Logger::warn(L"Failed to create low level mouse hook. {}", get_last_error_or_default(GetLastError()));
225      }
226  }
227  
228  void OverlayWindow::ShowWindow()
229  {
230      winkey_popup = std::make_unique<D2DOverlayWindow>();
231      winkey_popup->apply_overlay_opacity(overlayOpacity.value / 100.0f);
232      winkey_popup->set_theme(theme.value);
233  
234      // The press time only takes effect when the shortcut guide is activated by pressing the win key.
235      if (shouldReactToPressedWinKey.value)
236      {
237          winkey_popup->apply_press_time_for_global_windows_shortcuts(windowsKeyPressTimeForGlobalWindowsShortcuts.value);
238          winkey_popup->apply_press_time_for_taskbar_icon_shortcuts(windowsKeyPressTimeForTaskbarIconShortcuts.value);
239      }
240      else
241      {
242          winkey_popup->apply_press_time_for_global_windows_shortcuts(0);
243          winkey_popup->apply_press_time_for_taskbar_icon_shortcuts(0);
244      }
245  
246      target_state = std::make_unique<TargetState>();
247      try
248      {
249          winkey_popup->initialize();
250      }
251      catch (...)
252      {
253          Logger::critical("Winkey popup failed to initialize");
254          return;
255      }
256  
257      target_state->toggle_force_shown();
258  }
259  
260  void OverlayWindow::CloseWindow(HideWindowType type, int mainThreadId)
261  {
262      if (mainThreadId == 0)
263      {
264          mainThreadId = GetCurrentThreadId();
265      }
266  
267      if (this->winkey_popup)
268      {
269          if (shouldReactToPressedWinKey.value)
270          {
271              // Send a dummy key to prevent Start Menu from activating
272              INPUT dummyEvent[1] = {};
273              dummyEvent[0].type = INPUT_KEYBOARD;
274              dummyEvent[0].ki.wVk = 0xFF;
275              dummyEvent[0].ki.dwFlags = KEYEVENTF_KEYUP;
276              SendInput(1, dummyEvent, sizeof(INPUT));
277          }
278          this->winkey_popup->SetWindowCloseType(ToWstring(type));
279          Logger::trace(L"Terminating process");
280          PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
281      }
282  }
283  
284  bool OverlayWindow::IsDisabled()
285  {
286      WCHAR exePath[MAX_PATH] = L"";
287      overlay_window_instance->get_exe_path(activeWindow, exePath);
288      if (wcslen(exePath) > 0)
289      {
290          return is_disabled_app(exePath);
291      }
292  
293      return false;
294  }
295  
296  OverlayWindow::~OverlayWindow()
297  {
298      if (event_waiter)
299      {
300          event_waiter.reset();
301      }
302  
303      if (winkey_popup)
304      {
305          winkey_popup->hide();
306      }
307  
308      if (target_state)
309      {
310          target_state->exit();
311          target_state.reset();
312      }
313  
314      if (winkey_popup)
315      {
316          winkey_popup.reset();
317      }
318  
319      if (keyboardHook)
320      {
321          UnhookWindowsHookEx(keyboardHook);
322      }
323  }
324  
325  void OverlayWindow::on_held()
326  {
327      auto windowInfo = GetShortcutGuideWindowInfo(activeWindow);
328      if (windowInfo.disabled)
329      {
330          target_state->was_hidden();
331          return;
332      }
333      winkey_popup->show(windowInfo.hwnd, windowInfo.snappable);
334  }
335  
336  void OverlayWindow::quick_hide()
337  {
338      winkey_popup->quick_hide();
339  }
340  
341  void OverlayWindow::was_hidden()
342  {
343      target_state->was_hidden();
344  }
345  
346  bool OverlayWindow::overlay_visible() const
347  {
348      return target_state->active();
349  }
350  
351  bool OverlayWindow::win_key_activation() const
352  {
353      return shouldReactToPressedWinKey.value;
354  }
355  
356  void OverlayWindow::init_settings()
357  {
358      auto settings = GetSettings();
359      overlayOpacity.value = settings.overlayOpacity;
360      theme.value = settings.theme;
361      disabledApps.value = settings.disabledApps;
362      shouldReactToPressedWinKey.value = settings.shouldReactToPressedWinKey;
363      windowsKeyPressTimeForGlobalWindowsShortcuts.value = settings.windowsKeyPressTimeForGlobalWindowsShortcuts;
364      windowsKeyPressTimeForTaskbarIconShortcuts.value = settings.windowsKeyPressTimeForTaskbarIconShortcuts;
365      update_disabled_apps();
366  }
367  
368  bool OverlayWindow::is_disabled_app(wchar_t* exePath)
369  {
370      if (exePath == nullptr)
371      {
372          return false;
373      }
374  
375      auto exePathUpper = std::wstring(exePath);
376      CharUpperBuffW(exePathUpper.data(), static_cast<DWORD>(exePathUpper.length()));
377      for (const auto& row : disabled_apps_array)
378      {
379          const auto pos = exePathUpper.rfind(row);
380          const auto last_slash = exePathUpper.rfind('\\');
381          // Check that row occurs in disabled_apps_array, and its last occurrence contains in itself the first character after the last backslash.
382          if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
383          {
384              return true;
385          }
386      }
387      return false;
388  }
389  
390  void OverlayWindow::update_disabled_apps()
391  {
392      disabled_apps_array.clear();
393      auto disabledUppercase = disabledApps.value;
394      CharUpperBuffW(disabledUppercase.data(), static_cast<DWORD>(disabledUppercase.length()));
395      std::wstring_view view(disabledUppercase);
396      view = trim(view);
397      while (!view.empty())
398      {
399          auto pos = (std::min)(view.find_first_of(L"\r\n"), view.length());
400          disabled_apps_array.emplace_back(view.substr(0, pos));
401          view.remove_prefix(pos);
402          view = trim(view);
403      }
404  }
405  
406  void OverlayWindow::get_exe_path(HWND window, wchar_t* path)
407  {
408      if (disabled_apps_array.empty())
409      {
410          return;
411      }
412  
413      DWORD pid = 0;
414      GetWindowThreadProcessId(window, &pid);
415      if (pid != 0)
416      {
417          HANDLE processHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
418          if (processHandle && GetProcessImageFileName(processHandle, path, MAX_PATH) > 0)
419          {
420              CloseHandle(processHandle);
421          }
422      }
423  }
424  
425  ShortcutGuideSettings OverlayWindow::GetSettings() noexcept
426  {
427      ShortcutGuideSettings settings;
428      json::JsonObject properties;
429      try
430      {
431          PowerToysSettings::PowerToyValues settingsValues =
432              PowerToysSettings::PowerToyValues::load_from_settings_file(app_key);
433  
434          auto settingsObject = settingsValues.get_raw_json();
435          if (!settingsObject.GetView().Size())
436          {
437              return settings;
438          }
439  
440          properties = settingsObject.GetNamedObject(L"properties");
441      }
442      catch (...)
443      {
444          Logger::warn("Failed to read settings. Use default settings");
445          return settings;
446      }
447  
448      try
449      {
450          settings.hotkey = PowerToysSettings::HotkeyObject::from_json(properties.GetNamedObject(OpenShortcut::name)).to_string();
451      }
452      catch (...)
453      {
454      }
455  
456      try
457      {
458          settings.overlayOpacity = static_cast<int>(properties.GetNamedObject(OverlayOpacity::name).GetNamedNumber(L"value"));
459      }
460      catch (...)
461      {
462      }
463  
464      try
465      {
466          settings.shouldReactToPressedWinKey = properties.GetNamedObject(ShouldReactToPressedWinKey::name).GetNamedBoolean(L"value");
467      }
468      catch (...)
469      {
470      }
471  
472      try
473      {
474          settings.windowsKeyPressTimeForGlobalWindowsShortcuts = static_cast<int>(properties.GetNamedObject(WindowsKeyPressTimeForGlobalWindowsShortcuts::name).GetNamedNumber(L"value"));
475      }
476      catch (...)
477      {
478      }
479  
480      try
481      {
482          settings.windowsKeyPressTimeForTaskbarIconShortcuts = static_cast<int>(properties.GetNamedObject(WindowsKeyPressTimeForTaskbarIconShortcuts::name).GetNamedNumber(L"value"));
483      }
484      catch (...)
485      {
486      }
487  
488      try
489      {
490          settings.theme = (std::wstring)properties.GetNamedObject(Theme::name).GetNamedString(L"value");
491      }
492      catch (...)
493      {
494      }
495  
496      try
497      {
498          settings.disabledApps = (std::wstring)properties.GetNamedObject(DisabledApps::name).GetNamedString(L"value");
499      }
500      catch (...)
501      {
502      }
503  
504      return settings;
505  }