/ src / runner / hotkey_conflict_detector.cpp
hotkey_conflict_detector.cpp
  1  #include "pch.h"
  2  #include "hotkey_conflict_detector.h"
  3  #include <common/SettingsAPI/settings_helpers.h>
  4  #include <windows.h>
  5  #include <unordered_map>
  6  #include <cwchar>
  7  
  8  namespace HotkeyConflictDetector
  9  {
 10      Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut)
 11      {
 12          Hotkey hotkey;
 13  
 14          hotkey.win = (shortcut.modifiersMask & MOD_WIN) != 0;
 15          hotkey.ctrl = (shortcut.modifiersMask & MOD_CONTROL) != 0;
 16          hotkey.shift = (shortcut.modifiersMask & MOD_SHIFT) != 0;
 17          hotkey.alt = (shortcut.modifiersMask & MOD_ALT) != 0;
 18  
 19          hotkey.key = shortcut.vkCode > 255 ? 0 : static_cast<unsigned char>(shortcut.vkCode);
 20  
 21          return hotkey;
 22      }
 23  
 24      HotkeyConflictManager* HotkeyConflictManager::instance = nullptr;
 25      std::mutex HotkeyConflictManager::instanceMutex;
 26  
 27      HotkeyConflictManager& HotkeyConflictManager::GetInstance()
 28      {
 29          std::lock_guard<std::mutex> lock(instanceMutex);
 30          if (instance == nullptr)
 31          {
 32              instance = new HotkeyConflictManager();
 33          }
 34          return *instance;
 35      }
 36  
 37      HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID)
 38      {
 39          if (disabledHotkeys.find(_moduleName) != disabledHotkeys.end())
 40          {
 41              return HotkeyConflictType::NoConflict;
 42          }
 43  
 44          uint16_t handle = GetHotkeyHandle(_hotkey);
 45  
 46          if (handle == 0)
 47          {
 48              return HotkeyConflictType::NoConflict;
 49          }
 50  
 51          // The order is important, first to check sys conflict and then inapp conflict
 52          if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
 53          {
 54              return HotkeyConflictType::SystemConflict;
 55          }
 56          
 57          if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
 58          {
 59              return HotkeyConflictType::InAppConflict;
 60          }
 61  
 62          auto it = hotkeyMap.find(handle);
 63  
 64          if (it == hotkeyMap.end())
 65          {
 66              return HasConflictWithSystemHotkey(_hotkey) ?
 67                  HotkeyConflictType::SystemConflict :
 68                  HotkeyConflictType::NoConflict;
 69          }
 70  
 71          if (wcscmp(it->second.moduleName.c_str(), _moduleName) == 0 && it->second.hotkeyID == _hotkeyID)
 72          {
 73              // A shortcut matching its own assignment is not considered a conflict.
 74              return HotkeyConflictType::NoConflict;
 75          }
 76  
 77          return HotkeyConflictType::InAppConflict;
 78      }
 79  
 80      HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey)
 81      {
 82          uint16_t handle = GetHotkeyHandle(_hotkey);
 83  
 84          if (handle == 0)
 85          {
 86              return HotkeyConflictType::NoConflict;
 87          }
 88  
 89          // The order is important, first to check sys conflict and then inapp conflict
 90          if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
 91          {
 92              return HotkeyConflictType::SystemConflict;
 93          }
 94  
 95          if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
 96          {
 97              return HotkeyConflictType::InAppConflict;
 98          }
 99  
100          auto it = hotkeyMap.find(handle);
101  
102          if (it == hotkeyMap.end())
103          {
104              return HasConflictWithSystemHotkey(_hotkey) ?
105                         HotkeyConflictType::SystemConflict :
106                         HotkeyConflictType::NoConflict;
107          }
108  
109          return HotkeyConflictType::InAppConflict;
110      }
111  
112      // This function should only be called when a conflict has already been identified. 
113      // It returns a list of all conflicting shortcuts.
114      std::vector<HotkeyConflictInfo> HotkeyConflictManager::GetAllConflicts(Hotkey const& _hotkey)
115      {
116          std::vector<HotkeyConflictInfo> conflicts;
117          uint16_t handle = GetHotkeyHandle(_hotkey);
118  
119          // Check in-app conflicts first
120          auto inAppIt = inAppConflictHotkeyMap.find(handle);
121          if (inAppIt != inAppConflictHotkeyMap.end())
122          {
123              // Add all in-app conflicts
124              for (const auto& conflict : inAppIt->second)
125              {
126                  conflicts.push_back(conflict);
127              }
128  
129              return conflicts;
130          }
131  
132          // Check system conflicts
133          auto sysIt = sysConflictHotkeyMap.find(handle);
134          if (sysIt != sysConflictHotkeyMap.end())
135          {
136              HotkeyConflictInfo systemConflict;
137              systemConflict.hotkey = _hotkey;
138              systemConflict.moduleName = L"System";
139              systemConflict.hotkeyID = 0;
140  
141              conflicts.push_back(systemConflict);
142  
143              return conflicts;
144          }
145  
146          // Check if there's a successfully registered hotkey that would conflict
147          auto registeredIt = hotkeyMap.find(handle);
148          if (registeredIt != hotkeyMap.end())
149          {
150              conflicts.push_back(registeredIt->second);
151  
152              return conflicts;
153          }
154  
155          // If all the above conditions are ruled out, a system-level conflict is the only remaining explanation.
156          HotkeyConflictInfo systemConflict;
157          systemConflict.hotkey = _hotkey;
158          systemConflict.moduleName = L"System";
159          systemConflict.hotkeyID = 0;
160          conflicts.push_back(systemConflict);
161  
162          return conflicts;
163      }
164  
165      bool HotkeyConflictManager::AddHotkey(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID, bool isEnabled)
166      {
167          if (!isEnabled)
168          {
169              disabledHotkeys[_moduleName].push_back({ _hotkey, _moduleName, _hotkeyID });
170              return true;
171          }
172  
173          uint16_t handle = GetHotkeyHandle(_hotkey);
174  
175          if (handle == 0)
176          {
177              return false;
178          }
179  
180          HotkeyConflictType conflictType = HasConflict(_hotkey, _moduleName, _hotkeyID);
181          if (conflictType != HotkeyConflictType::NoConflict)
182          {
183              if (conflictType == HotkeyConflictType::InAppConflict)
184              {
185                  auto hotkeyFound = hotkeyMap.find(handle);
186                  inAppConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID });
187  
188                  if (hotkeyFound != hotkeyMap.end())
189                  {
190                      inAppConflictHotkeyMap[handle].insert(hotkeyFound->second);
191                      hotkeyMap.erase(hotkeyFound);
192                  }
193              }
194              else
195              {
196                  sysConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID });
197              }
198              return false;
199          }
200  
201          HotkeyConflictInfo hotkeyInfo;
202          hotkeyInfo.moduleName = _moduleName;
203          hotkeyInfo.hotkeyID = _hotkeyID;
204          hotkeyInfo.hotkey = _hotkey;
205          hotkeyMap[handle] = hotkeyInfo;
206  
207          return true;
208      }
209  
210      std::vector<HotkeyConflictInfo> HotkeyConflictManager::RemoveHotkeyByModule(const std::wstring& moduleName)
211      {
212          std::vector<HotkeyConflictInfo> removedHotkeys;
213  
214          if (disabledHotkeys.find(moduleName) != disabledHotkeys.end())
215          {
216              disabledHotkeys.erase(moduleName);
217          }
218  
219          std::lock_guard<std::mutex> lock(hotkeyMutex);
220          bool foundRecord = false;
221  
222          for (auto it = sysConflictHotkeyMap.begin(); it != sysConflictHotkeyMap.end();)
223          {
224              auto& conflictSet = it->second;
225              for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
226              {
227                  if (setIt->moduleName == moduleName)
228                  {
229                      removedHotkeys.push_back(*setIt);
230                      setIt = conflictSet.erase(setIt);
231                      foundRecord = true;
232                  }
233                  else
234                  {
235                      ++setIt;
236                  }
237              }
238              if (conflictSet.empty())
239              {
240                  it = sysConflictHotkeyMap.erase(it);
241              }
242              else
243              {
244                  ++it;
245              }
246          }
247  
248          for (auto it = inAppConflictHotkeyMap.begin(); it != inAppConflictHotkeyMap.end();)
249          {
250              auto& conflictSet = it->second;
251              uint16_t handle = it->first;
252  
253              for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
254              {
255                  if (setIt->moduleName == moduleName)
256                  {
257                      removedHotkeys.push_back(*setIt);
258                      setIt = conflictSet.erase(setIt);
259                      foundRecord = true;
260                  }
261                  else
262                  {
263                      ++setIt;
264                  }
265              }
266  
267              if (conflictSet.empty())
268              {
269                  it = inAppConflictHotkeyMap.erase(it);
270              }
271              else if (conflictSet.size() == 1)
272              {
273                  // Move the only remaining conflict to main map
274                  const auto& onlyConflict = *conflictSet.begin();
275                  hotkeyMap[handle] = onlyConflict;
276                  it = inAppConflictHotkeyMap.erase(it);
277              }
278              else
279              {
280                  ++it;
281              }
282          }
283  
284          for (auto it = hotkeyMap.begin(); it != hotkeyMap.end();)
285          {
286              if (it->second.moduleName == moduleName)
287              {
288                  uint16_t handle = it->first;
289                  removedHotkeys.push_back(it->second);
290                  it = hotkeyMap.erase(it);
291                  foundRecord = true;
292  
293                  auto inAppIt = inAppConflictHotkeyMap.find(handle);
294                  if (inAppIt != inAppConflictHotkeyMap.end() && inAppIt->second.size() == 1)
295                  {
296                      // Move the only in-app conflict to main map
297                      const auto& onlyConflict = *inAppIt->second.begin();
298                      hotkeyMap[handle] = onlyConflict;
299                      inAppConflictHotkeyMap.erase(inAppIt);
300                  }
301              }
302              else
303              {
304                  ++it;
305              }
306          }
307  
308          return removedHotkeys;
309      }
310  
311      void HotkeyConflictManager::EnableHotkeyByModule(const std::wstring& moduleName)
312      {
313          if (disabledHotkeys.find(moduleName) == disabledHotkeys.end())
314          {
315              return; // No disabled hotkeys for this module
316          }
317  
318          auto hotkeys = disabledHotkeys[moduleName];
319          disabledHotkeys.erase(moduleName);
320  
321          for (const auto& hotkeyInfo : hotkeys)
322          {
323              // Re-add the hotkey as enabled
324              AddHotkey(hotkeyInfo.hotkey, moduleName.c_str(), hotkeyInfo.hotkeyID, true);
325          }   
326      }
327  
328      void HotkeyConflictManager::DisableHotkeyByModule(const std::wstring& moduleName)
329      {
330          auto hotkeys = RemoveHotkeyByModule(moduleName);
331          disabledHotkeys[moduleName] = hotkeys;
332      }
333  
334      bool HotkeyConflictManager::HasConflictWithSystemHotkey(const Hotkey& hotkey)
335      {
336          // Convert PowerToys Hotkey format to Win32 RegisterHotKey format
337          UINT modifiers = 0;
338          if (hotkey.win)
339          {
340              modifiers |= MOD_WIN;
341          }
342          if (hotkey.ctrl)
343          {
344              modifiers |= MOD_CONTROL;
345          }
346          if (hotkey.alt)
347          {
348              modifiers |= MOD_ALT;
349          }
350          if (hotkey.shift)
351          {
352              modifiers |= MOD_SHIFT;
353          }
354  
355          // No modifiers or no key is not a valid hotkey
356          if (modifiers == 0 || hotkey.key == 0)
357          {
358              return false;
359          }
360  
361          // Use a unique ID for this test registration
362          const int hotkeyId = 0x0FFF; // Arbitrary ID for temporary registration
363  
364          // Try to register the hotkey with Windows, using nullptr instead of a window handle
365          if (!RegisterHotKey(nullptr, hotkeyId, modifiers, hotkey.key))
366          {
367              // If registration fails with ERROR_HOTKEY_ALREADY_REGISTERED, it means the hotkey
368              // is already in use by the system or another application
369              if (GetLastError() == ERROR_HOTKEY_ALREADY_REGISTERED)
370              {
371                  return true;
372              }
373          }
374          else
375          {
376              // If registration succeeds, unregister it immediately
377              UnregisterHotKey(nullptr, hotkeyId);
378          }
379  
380          return false;
381      }
382  
383      json::JsonObject HotkeyConflictManager::GetHotkeyConflictsAsJson()
384      {
385          std::lock_guard<std::mutex> lock(hotkeyMutex);
386  
387          using namespace json;
388          JsonObject root;
389  
390          // Serialize hotkey to a unique string format for grouping
391          auto serializeHotkey = [](const Hotkey& hotkey) -> JsonObject {
392              JsonObject obj;
393              obj.Insert(L"win", value(hotkey.win));
394              obj.Insert(L"ctrl", value(hotkey.ctrl));
395              obj.Insert(L"shift", value(hotkey.shift));
396              obj.Insert(L"alt", value(hotkey.alt));
397              obj.Insert(L"key", value(static_cast<int>(hotkey.key)));
398              return obj;
399          };
400  
401          // New format: Group conflicts by hotkey
402          JsonArray inAppConflictsArray;
403          JsonArray sysConflictsArray;
404  
405          // Process in-app conflicts - only include hotkeys that are actually in conflict
406          for (const auto& [handle, conflicts] : inAppConflictHotkeyMap)
407          {
408              if (!conflicts.empty())
409              {
410                  JsonObject conflictGroup;
411  
412                  // All entries have the same hotkey, so use the first one for the key
413                  conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
414  
415                  // Create an array of module info without repeating the hotkey
416                  JsonArray modules;
417                  for (const auto& info : conflicts)
418                  {
419                      JsonObject moduleInfo;
420                      moduleInfo.Insert(L"moduleName", value(info.moduleName));
421                      moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID));
422                      modules.Append(moduleInfo);
423                  }
424  
425                  conflictGroup.Insert(L"modules", modules);
426                  inAppConflictsArray.Append(conflictGroup);
427              }
428          }
429  
430          // Process system conflicts - only include hotkeys that are actually in conflict
431          for (const auto& [handle, conflicts] : sysConflictHotkeyMap)
432          {
433              if (!conflicts.empty())
434              {
435                  JsonObject conflictGroup;
436  
437                  // All entries have the same hotkey, so use the first one for the key
438                  conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
439  
440                  // Create an array of module info without repeating the hotkey
441                  JsonArray modules;
442                  for (const auto& info : conflicts)
443                  {
444                      JsonObject moduleInfo;
445                      moduleInfo.Insert(L"moduleName", value(info.moduleName));
446                      moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID));
447                      modules.Append(moduleInfo);
448                  }
449  
450                  conflictGroup.Insert(L"modules", modules);
451                  sysConflictsArray.Append(conflictGroup);
452              }
453          }
454  
455          // Add the grouped conflicts to the root object
456          root.Insert(L"inAppConflicts", inAppConflictsArray);
457          root.Insert(L"sysConflicts", sysConflictsArray);
458  
459          return root;
460      }
461  
462      uint16_t HotkeyConflictManager::GetHotkeyHandle(const Hotkey& hotkey)
463      {
464          uint16_t handle = hotkey.key;
465          handle |= hotkey.win << 8;
466          handle |= hotkey.ctrl << 9;
467          handle |= hotkey.shift << 10;
468          handle |= hotkey.alt << 11;
469          return handle;
470      }
471  }