KeyboardListener.cpp
1 #include "pch.h" 2 #include "KeyboardListener.h" 3 #include "KeyboardListener.g.cpp" 4 5 #include <common/logger/logger.h> 6 #include <common/utils/logger_helper.h> 7 #include <common/utils/winapi_error.h> 8 #include <common/utils/string_utils.h> 9 #include <common/utils/process_path.h> 10 #include <common/utils/excluded_apps.h> 11 #include <common/utils/game_mode.h> 12 13 namespace winrt::PowerToys::PowerAccentKeyboardService::implementation 14 { 15 KeyboardListener::KeyboardListener() : 16 m_toolbarVisible(false), m_triggeredWithSpace(false), m_leftShiftPressed(false), m_rightShiftPressed(false), m_triggeredWithLeftArrow(false), m_triggeredWithRightArrow(false) 17 { 18 s_instance = this; 19 LoggerHelpers::init_logger(L"PowerAccent", L"PowerAccentKeyboardService", "PowerAccent"); 20 } 21 22 void KeyboardListener::InitHook() 23 { 24 #if defined(DISABLE_LOWLEVEL_HOOKS_WHEN_DEBUGGED) 25 const bool hook_disabled = IsDebuggerPresent(); 26 #else 27 const bool hook_disabled = false; 28 #endif 29 30 if (!hook_disabled) 31 { 32 s_llKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(NULL), NULL); 33 if (!s_llKeyboardHook) 34 { 35 DWORD errorCode = GetLastError(); 36 show_last_error_message(L"SetWindowsHookEx", errorCode, L"PowerToys - PowerAccent"); 37 auto errorMessage = get_last_error_message(errorCode); 38 Logger::error(errorMessage.has_value() ? errorMessage.value() : L""); 39 } 40 } 41 } 42 43 void KeyboardListener::UnInitHook() 44 { 45 if (s_llKeyboardHook) 46 { 47 if (UnhookWindowsHookEx(s_llKeyboardHook)) 48 { 49 s_llKeyboardHook = nullptr; 50 } 51 } 52 } 53 54 void KeyboardListener::SetShowToolbarEvent(ShowToolbar showToolbarEvent) 55 { 56 m_showToolbarCb = [trigger = std::move(showToolbarEvent)](LetterKey key) { 57 trigger(key); 58 }; 59 } 60 61 void KeyboardListener::SetHideToolbarEvent(HideToolbar hideToolbarEvent) 62 { 63 m_hideToolbarCb = [trigger = std::move(hideToolbarEvent)](InputType inputType) { 64 trigger(inputType); 65 }; 66 } 67 68 void KeyboardListener::SetNextCharEvent(NextChar nextCharEvent) 69 { 70 m_nextCharCb = [trigger = std::move(nextCharEvent)](TriggerKey triggerKey, bool shiftPressed) { 71 trigger(triggerKey, shiftPressed); 72 }; 73 } 74 75 void KeyboardListener::SetIsLanguageLetterDelegate(IsLanguageLetter isLanguageLetterDelegate) 76 { 77 m_isLanguageLetterCb = [trigger = std::move(isLanguageLetterDelegate)](LetterKey key) { 78 bool result; 79 trigger(key, result); 80 return result; 81 }; 82 } 83 84 void KeyboardListener::UpdateActivationKey(int32_t activationKey) 85 { 86 m_settings.activationKey = static_cast<PowerAccentActivationKey>(activationKey); 87 } 88 89 void KeyboardListener::UpdateDoNotActivateOnGameMode(bool doNotActivateOnGameMode) 90 { 91 m_settings.doNotActivateOnGameMode = doNotActivateOnGameMode; 92 } 93 94 void KeyboardListener::UpdateInputTime(int32_t inputTime) 95 { 96 m_settings.inputTime = std::chrono::milliseconds(inputTime); 97 } 98 99 void KeyboardListener::UpdateExcludedApps(std::wstring_view excludedAppsView) 100 { 101 std::vector<std::wstring> excludedApps; 102 auto excludedUppercase = std::wstring(excludedAppsView); 103 CharUpperBuffW(excludedUppercase.data(), static_cast<DWORD>(excludedUppercase.length())); 104 std::wstring_view view(excludedUppercase); 105 view = left_trim<wchar_t>(trim<wchar_t>(view)); 106 107 while (!view.empty()) 108 { 109 auto pos = (std::min)(view.find_first_of(L"\r\n"), view.length()); 110 excludedApps.emplace_back(view.substr(0, pos)); 111 view.remove_prefix(pos); 112 view = left_trim<wchar_t>(trim<wchar_t>(view)); 113 } 114 { 115 std::lock_guard<std::mutex> lock(m_mutex_excluded_apps); 116 m_settings.excludedApps = std::move(excludedApps); 117 m_prevForegroundAppExcl = { NULL, false }; 118 } 119 } 120 121 bool KeyboardListener::IsSuppressedByGameMode() 122 { 123 return m_settings.doNotActivateOnGameMode && detect_game_mode(); 124 } 125 126 bool KeyboardListener::IsForegroundAppExcluded() 127 { 128 std::lock_guard<std::mutex> lock(m_mutex_excluded_apps); 129 130 if (m_settings.excludedApps.empty()) 131 { 132 m_prevForegroundAppExcl = { NULL, false }; 133 return false; 134 } 135 136 if (HWND foregroundApp{ GetForegroundWindow() }) 137 { 138 if (m_prevForegroundAppExcl.first == foregroundApp) 139 { 140 return m_prevForegroundAppExcl.second; 141 } 142 auto processPath = get_process_path(foregroundApp); 143 CharUpperBuffW(processPath.data(), static_cast<DWORD>(processPath.length())); 144 m_prevForegroundAppExcl = { foregroundApp, 145 check_excluded_app(foregroundApp, processPath, m_settings.excludedApps) }; 146 147 return m_prevForegroundAppExcl.second; 148 } 149 150 m_prevForegroundAppExcl = { NULL, false }; 151 152 return false; 153 } 154 155 bool KeyboardListener::OnKeyDown(KBDLLHOOKSTRUCT info) noexcept 156 { 157 auto letterKey = static_cast<LetterKey>(info.vkCode); 158 159 // Shift key is detected only if the toolbar is already visible to avoid conflicts with uppercase 160 if (!m_leftShiftPressed && m_toolbarVisible && info.vkCode == VK_LSHIFT) 161 { 162 m_leftShiftPressed = true; 163 } 164 165 if (!m_rightShiftPressed && m_toolbarVisible && info.vkCode == VK_RSHIFT) 166 { 167 m_rightShiftPressed = true; 168 } 169 170 if (std::find(letters.begin(), letters.end(), letterKey) != cend(letters) && m_isLanguageLetterCb(letterKey)) 171 { 172 if (m_toolbarVisible && letterPressed == letterKey) 173 { 174 // On-screen keyboard continuously sends WM_KEYDOWN when a key is held down 175 // If Quick Accent is visible, prevent the letter key from being processed 176 // https://github.com/microsoft/PowerToys/issues/36853 177 return true; 178 } 179 180 m_stopwatch.reset(); 181 letterPressed = letterKey; 182 } 183 184 UINT triggerPressed = 0; 185 if (letterPressed != LetterKey::None) 186 { 187 if (std::find(std::begin(triggers), end(triggers), static_cast<TriggerKey>(info.vkCode)) != end(triggers)) 188 { 189 triggerPressed = info.vkCode; 190 const bool isLetterReleased = (GetAsyncKeyState((int)letterPressed) & 0x8000) == 0; 191 192 if (isLetterReleased || 193 (triggerPressed == VK_SPACE && m_settings.activationKey == PowerAccentActivationKey::LeftRightArrow) || 194 ((triggerPressed == VK_LEFT || triggerPressed == VK_RIGHT) && m_settings.activationKey == PowerAccentActivationKey::Space)) 195 { 196 Logger::debug(L"Reset trigger key"); 197 return false; 198 } 199 } 200 } 201 202 if (!m_toolbarVisible && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded()) 203 { 204 Logger::debug(L"Show toolbar. Letter: {}, Trigger: {}", letterPressed, triggerPressed); 205 206 // Keep track if it was triggered with space so that it can be typed on false starts. 207 m_triggeredWithSpace = triggerPressed == VK_SPACE; 208 m_triggeredWithLeftArrow = triggerPressed == VK_LEFT; 209 m_triggeredWithRightArrow = triggerPressed == VK_RIGHT; 210 m_toolbarVisible = true; 211 m_showToolbarCb(letterPressed); 212 } 213 214 if (m_toolbarVisible && triggerPressed) 215 { 216 if (triggerPressed == VK_LEFT) 217 { 218 Logger::debug(L"Next toolbar position - left"); 219 m_nextCharCb(TriggerKey::Left, m_leftShiftPressed || m_rightShiftPressed); 220 } 221 else if (triggerPressed == VK_RIGHT) 222 { 223 Logger::debug(L"Next toolbar position - right"); 224 m_nextCharCb(TriggerKey::Right, m_leftShiftPressed || m_rightShiftPressed); 225 } 226 else if (triggerPressed == VK_SPACE) 227 { 228 Logger::debug(L"Next toolbar position - space"); 229 m_nextCharCb(TriggerKey::Space, m_leftShiftPressed || m_rightShiftPressed); 230 } 231 232 return true; 233 } 234 235 return false; 236 } 237 238 bool KeyboardListener::OnKeyUp(KBDLLHOOKSTRUCT info) noexcept 239 { 240 if (m_leftShiftPressed && info.vkCode == VK_LSHIFT) 241 { 242 m_leftShiftPressed = false; 243 } 244 245 if (m_rightShiftPressed && info.vkCode == VK_RSHIFT) 246 { 247 m_rightShiftPressed = false; 248 } 249 250 if (std::find(std::begin(letters), end(letters), static_cast<LetterKey>(info.vkCode)) != end(letters) && m_isLanguageLetterCb(static_cast<LetterKey>(info.vkCode))) 251 { 252 letterPressed = LetterKey::None; 253 254 if (m_toolbarVisible) 255 { 256 if (m_stopwatch.elapsed() < m_settings.inputTime) 257 { 258 Logger::debug(L"Activation too fast. Do nothing."); 259 260 // False start, we should output the space if it was the trigger. 261 if (m_triggeredWithSpace) 262 { 263 m_hideToolbarCb(InputType::Space); 264 } 265 else if (m_triggeredWithLeftArrow) 266 { 267 m_hideToolbarCb(InputType::Left); 268 } 269 else if (m_triggeredWithRightArrow) 270 { 271 m_hideToolbarCb(InputType::Right); 272 } 273 else 274 { 275 m_hideToolbarCb(InputType::None); 276 } 277 m_toolbarVisible = false; 278 return true; 279 } 280 Logger::debug(L"Hide toolbar event and input char"); 281 282 m_hideToolbarCb(InputType::Char); 283 284 m_toolbarVisible = false; 285 } 286 } 287 288 return false; 289 } 290 291 LRESULT KeyboardListener::LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) 292 { 293 if (nCode == HC_ACTION && s_instance != nullptr) 294 { 295 KBDLLHOOKSTRUCT* key = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam); 296 switch (wParam) 297 { 298 case WM_KEYDOWN: 299 { 300 if (s_instance->OnKeyDown(*key)) 301 { 302 return true; 303 } 304 } 305 break; 306 case WM_KEYUP: 307 { 308 if (s_instance->OnKeyUp(*key)) 309 { 310 return true; 311 } 312 } 313 break; 314 } 315 } 316 317 return CallNextHookEx(NULL, nCode, wParam, lParam); 318 } 319 }