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 }