/ src / modules / poweraccent / PowerAccentKeyboardService / KeyboardListener.cpp
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  }