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 }