From 75526b9580d4965a92c66fcd46a86d2c0b22895d Mon Sep 17 00:00:00 2001
From: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Date: Wed, 20 Aug 2025 09:31:52 +0800
Subject: [PATCH] [Feature] PowerToys hotkey conflict detection (#41029)
## Summary of the Pull Request
Implements comprehensive hotkey conflict detection and resolution system
for PowerToys, providing real-time conflict checking and centralized
management interface.
## PR Checklist
- [ ] **Closes:** #xxx
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [x] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: [Shortcut conflict detction dev
spec](https://github.com/MicrosoftDocs/windows-dev-docs/pull/5519)
## TODO Lists
- [x] Add real-time hotkey validation functionality to the hotkey dialog
- [x] Immediately detect conflicts and update shortcut conflict status
after applying new shortcuts
- [x] Return conflict list from runner hotkey conflict detector for
conflict checking.
- [x] Implement the Tooltip for every shortcut control
- [x] Add dialog UI for showing all the shortcut conflicts
- [x] Support changing shortcut directly inside the shortcut conflict
window/dialog, no need to nav to the settings page.
- [x] Redesign the `ShortcutConflictDialogContentControl` to align with
the spec
- [x] Add navigating and changing hotkey auctionability to the
`ShortcutConflictDialogContentControl`
- [x] Add telemetry. Impemented in [another
PR](https://github.com/shuaiyuanxx/PowerToys/pull/47)
## Shortcut Conflict Support Modules

Demo videos
https://github.com/user-attachments/assets/476d992c-c6ca-4bcd-a3f2-b26cc612d1b9
https://github.com/user-attachments/assets/1c1a2537-de54-4db2-bdbf-6f1908ff1ce7
https://github.com/user-attachments/assets/9c992254-fc2b-402c-beec-20fceef25e6b
https://github.com/user-attachments/assets/d66abc1c-b8bf-45f8-a552-ec989dab310f
## Detailed Description of the Pull Request / Additional comments
## Validation Steps Performed
Manually validation performed.
---------
Signed-off-by: Shawn Yuan
Signed-off-by: Shuai Yuan
Co-authored-by: Niels Laute
---
.github/actions/spell-check/expect.txt | 32 ++
.../core/settings/settings-implementation.md | 35 ++
.../AdvancedPasteModuleInterface/dllmain.cpp | 48 +-
.../ModuleInterface/dllmain.cpp | 55 ++
.../interface/powertoy_module_interface.h | 32 +-
src/runner/centralized_hotkeys.h | 4 +-
src/runner/general_settings.cpp | 7 +
src/runner/hotkey_conflict_detector.cpp | 471 ++++++++++++++++++
src/runner/hotkey_conflict_detector.h | 100 ++++
src/runner/powertoy_module.cpp | 22 +-
src/runner/powertoy_module.h | 9 +
src/runner/runner.vcxproj | 2 +
src/runner/runner.vcxproj.filters | 6 +
src/runner/settings_window.cpp | 74 +++
.../AdvancedPasteAdditionalAction.cs | 18 +-
.../AdvancedPasteAdditionalActions.cs | 17 +-
.../AdvancedPasteCustomAction.cs | 21 +-
.../AdvancedPasteSettings.cs | 64 ++-
.../AlwaysOnTopSettings.cs | 21 +-
.../ColorPickerSettings.cs | 19 +-
.../ColorPickerSettingsVersion1.cs | 1 -
.../CropAndLockSettings.cs | 25 +-
.../FindMyMouseSettings.cs | 21 +-
.../Helpers/HotkeyAccessor.cs | 34 ++
.../HotkeyConflicts/AllHotkeyConflictsData.cs | 19 +
.../AllHotkeyConflictsEventArgs.cs | 22 +
.../HotkeyConflictGroupData.cs | 21 +
.../HotkeyConflicts/HotkeyConflictInfo.cs | 23 +
.../HotkeyConflicts/HotkeyData.cs | 71 +++
.../HotkeyConflicts/ModuleConflictsData.cs | 21 +
.../HotkeyConflicts/ModuleHotkeyData.cs | 84 ++++
.../Settings.UI.Library/HotkeySettings.cs | 63 ++-
.../Interfaces/IHotkeyConfig.cs | 17 +
.../MeasureToolSettings.cs | 21 +-
.../MouseHighlighterSettings.cs | 21 +-
.../Settings.UI.Library/MouseJumpSettings.cs | 21 +-
.../MousePointerCrosshairsSettings.cs | 21 +-
.../MouseWithoutBordersSettings.cs | 33 +-
.../Settings.UI.Library/PeekSettings.cs | 21 +-
.../PowerLauncherSettings.cs | 20 +-
.../Settings.UI.Library/PowerOcrSettings.cs | 21 +-
.../Settings.UI.Library/SettingsFactory.cs | 197 ++++++++
.../ShortcutGuideSettings.cs | 21 +-
.../Settings.UI.Library/WorkspacesSettings.cs | 21 +-
.../PowerLauncherViewModelTest.cs | 36 +-
.../Converters/BoolToConflictTypeConverter.cs | 27 +
.../Helpers/HotkeyConflictHelper.cs | 73 +++
.../Helpers/HotkeyConflictResponse.cs | 22 +
.../SourceGenerationContextContext.cs | 14 +-
.../Services/GlobalHotkeyConflictManager.cs | 121 +++++
.../Services/IPCResponseService.cs | 199 ++++++++
.../Settings.UI/SettingsXAML/App.xaml.cs | 20 +-
.../Dashboard/ShortcutConflictControl.xaml | 8 +-
.../Dashboard/ShortcutConflictControl.xaml.cs | 117 ++++-
.../Dashboard/ShortcutConflictWindow.xaml | 176 +++++++
.../Dashboard/ShortcutConflictWindow.xaml.cs | 91 ++++
.../Controls/KeyVisual/KeyVisual.xaml | 2 +-
.../ShortcutControl/ShortcutControl.xaml | 2 +
.../ShortcutControl/ShortcutControl.xaml.cs | 204 +++++++-
.../ShortcutDialogContentControl.xaml | 9 +-
.../ShortcutDialogContentControl.xaml.cs | 24 +-
.../ShortcutWithTextLabelControl.xaml | 16 +-
.../ShortcutWithTextLabelControl.xaml.cs | 33 +-
.../SettingsXAML/OOBE/Views/OobeOverview.xaml | 53 +-
.../OOBE/Views/OobeOverview.xaml.cs | 196 +++++++-
.../SettingsXAML/OOBE/Views/OobeWhatsNew.xaml | 300 ++++++-----
.../OOBE/Views/OobeWhatsNew.xaml.cs | 75 ++-
.../Settings.UI/SettingsXAML/OobeWindow.xaml | 2 +-
.../SettingsXAML/Views/AdvancedPaste.xaml.cs | 2 +
.../Views/AlwaysOnTopPage.xaml.cs | 2 +
.../SettingsXAML/Views/CmdPalPage.xaml.cs | 1 +
.../Views/ColorPickerPage.xaml.cs | 2 +
.../Views/CropAndLockPage.xaml.cs | 2 +
.../SettingsXAML/Views/DashboardPage.xaml | 2 +-
.../SettingsXAML/Views/DashboardPage.xaml.cs | 2 +
.../SettingsXAML/Views/FancyZonesPage.xaml.cs | 1 +
.../Views/MeasureToolPage.xaml.cs | 2 +
.../SettingsXAML/Views/MouseUtilsPage.xaml.cs | 2 +
.../Views/MouseWithoutBordersPage.xaml.cs | 2 +
.../SettingsXAML/Views/PeekPage.xaml.cs | 1 +
.../Views/PowerLauncherPage.xaml.cs | 3 +
.../SettingsXAML/Views/PowerOcrPage.xaml.cs | 1 +
.../SettingsXAML/Views/ShellPage.xaml.cs | 1 +
.../Views/ShortcutGuidePage.xaml.cs | 2 +
.../SettingsXAML/Views/WorkspacesPage.xaml.cs | 1 +
.../Settings.UI/Strings/en-us/Resources.resw | 73 ++-
.../ViewModels/AdvancedPasteViewModel.cs | 77 ++-
.../ViewModels/AlwaysOnTopViewModel.cs | 16 +-
.../Settings.UI/ViewModels/CmdPalViewModel.cs | 16 +-
.../ViewModels/ColorPickerViewModel.cs | 38 +-
.../ViewModels/CropAndLockViewModel.cs | 17 +-
.../ViewModels/DashboardViewModel.cs | 40 +-
.../ViewModels/FancyZonesViewModel.cs | 25 +-
.../ViewModels/MeasureToolViewModel.cs | 17 +-
.../ViewModels/MouseUtilsViewModel.cs | 23 +-
.../MouseUtilsViewModel_MouseJump.cs | 3 +-
.../MouseWithoutBordersViewModel.cs | 95 +++-
.../ViewModels/PageViewModelBase.cs | 251 ++++++++++
.../Settings.UI/ViewModels/PeekViewModel.cs | 35 +-
.../ViewModels/PowerLauncherViewModel.cs | 17 +-
.../ViewModels/PowerOcrViewModel.cs | 32 +-
.../ViewModels/ShortcutConflictViewModel.cs | 384 ++++++++++++++
.../ViewModels/ShortcutGuideViewModel.cs | 22 +-
.../ViewModels/WorkspacesViewModel.cs | 17 +-
104 files changed, 4578 insertions(+), 366 deletions(-)
create mode 100644 src/runner/hotkey_conflict_detector.cpp
create mode 100644 src/runner/hotkey_conflict_detector.h
create mode 100644 src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs
create mode 100644 src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs
create mode 100644 src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs
create mode 100644 src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs
create mode 100644 src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs
create mode 100644 src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs
create mode 100644 src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleConflictsData.cs
create mode 100644 src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs
create mode 100644 src/settings-ui/Settings.UI.Library/Interfaces/IHotkeyConfig.cs
create mode 100644 src/settings-ui/Settings.UI.Library/SettingsFactory.cs
create mode 100644 src/settings-ui/Settings.UI/Converters/BoolToConflictTypeConverter.cs
create mode 100644 src/settings-ui/Settings.UI/Helpers/HotkeyConflictHelper.cs
create mode 100644 src/settings-ui/Settings.UI/Helpers/HotkeyConflictResponse.cs
create mode 100644 src/settings-ui/Settings.UI/Services/GlobalHotkeyConflictManager.cs
create mode 100644 src/settings-ui/Settings.UI/Services/IPCResponseService.cs
create mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml
create mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs
create mode 100644 src/settings-ui/Settings.UI/ViewModels/PageViewModelBase.cs
create mode 100644 src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 7e460dba2f..7ea012fe0e 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -25,6 +25,8 @@ ADMINS
adml
admx
advancedpaste
+advancedpasteui
+advancedpasteuishortcut
advfirewall
AFeature
affordances
@@ -40,6 +42,7 @@ ALLINPUT
Allman
Allmodule
ALLOWUNDO
+allpc
ALLVIEW
ALPHATYPE
AModifier
@@ -629,6 +632,7 @@ HKCU
hkey
HKLM
HKM
+hkmng
HKPD
HKU
HMD
@@ -646,7 +650,11 @@ Hostx
hotfixes
hotkeycontrol
HOTKEYF
+hotkeylockmachine
+hotkeyreconnect
hotkeys
+hotkeyswitch
+hotkeytoggleeasymouse
hotlight
hotspot
HPAINTBUFFER
@@ -704,9 +712,12 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
+imagetotext
+imagetotextshortcut
imagingdevices
ime
imgflip
+inapp
inbox
INCONTACT
Indo
@@ -789,6 +800,7 @@ keyvault
KILLFOCUS
killrunner
kmph
+kvp
Kybd
lastcodeanalysissucceeded
LASTEXITCODE
@@ -827,6 +839,7 @@ localappdata
localpackage
LOCALSYSTEM
LOCATIONCHANGE
+LOCKMACHINE
LOCKTYPE
LOGFONT
LOGFONTW
@@ -912,6 +925,7 @@ MDL
mdtext
mdtxt
mdwn
+measuretool
meme
memicmp
MENUITEMINFO
@@ -961,6 +975,7 @@ MOUSEHWHEEL
MOUSEINPUT
mousejump
mousepointer
+mousepointercrosshairs
mouseutils
MOVESIZEEND
MOVESIZESTART
@@ -1161,6 +1176,18 @@ PARENTRELATIVEFORADDRESSBAR
PARENTRELATIVEPARSING
parray
PARTIALCONFIRMATIONDIALOGTITLE
+pasteashtmlfile
+pasteashtmlfileshortcut
+pasteasjson
+pasteasjsonshortcut
+pasteasmarkdown
+pasteasmarkdownshortcut
+pasteasplaintext
+pasteasplaintextshortcut
+pasteaspngfile
+pasteaspngfileshortcut
+pasteastxtfile
+pasteastxtfileshortcut
PATCOPY
PATHMUSTEXIST
PATINVERT
@@ -1228,6 +1255,7 @@ Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
+powerocr
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1368,6 +1396,7 @@ Removelnk
renamable
RENAMEONCOLLISION
reparented
+reparenthotkey
reparenting
reportfileaccesses
requery
@@ -1687,6 +1716,7 @@ THH
THICKFRAME
THISCOMPONENT
throughs
+thumbnailhotkey
TILEDWINDOW
TILLSON
timedate
@@ -1701,6 +1731,7 @@ tlb
tlbimp
tlc
TNP
+TOGGLEEASYMOUSE
Toolhelp
toolkitconverters
toolwindow
@@ -1714,6 +1745,7 @@ tracelogging
tracerpt
trackbar
trafficmanager
+transcodetomp
transicc
TRAYMOUSEMESSAGE
triaging
diff --git a/doc/devdocs/core/settings/settings-implementation.md b/doc/devdocs/core/settings/settings-implementation.md
index d97aff2dac..defe59a3fa 100644
--- a/doc/devdocs/core/settings/settings-implementation.md
+++ b/doc/devdocs/core/settings/settings-implementation.md
@@ -71,6 +71,41 @@ When the user changes settings in the UI:
3. The runner calls the `set_config` function on the appropriate module
4. The module parses the JSON and applies the new settings
+# Shortcut Conflict Detection
+
+Steps to enable conflict detection for a hotkey:
+
+### 1. Implement module interface for hotkeys
+Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional GetHotkeyEx()`.
+
+- If not yet implemented, you need to add it so that it returns all hotkeys used by the module.
+- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup.
+- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp`
+
+### 2. Implement IHotkeyConfig in the module settings (UI side)
+Make sure the module’s settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`.
+
+- This method should return all hotkeys used in the module.
+- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`).
+- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs`
+- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings.
+It provides both `getter` and `setter` methods to read and update the corresponding hotkey.
+Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey.
+This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`
+
+### 3. Update the module’s ViewModel
+The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary GetAllHotkeySettings()`.
+
+- This method should return all hotkeys, maintaining the same order as in steps 1 and 2.
+- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs`
+
+### 4. Ensure the module’s Views call `OnPageLoaded()`
+Once the module’s view is loaded, make sure to invoke the ViewModel’s `OnPageLoaded()` method:
+```cs
+Loaded += (s, e) => ViewModel.OnPageLoaded();
+```
+- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
+
## Debugging Settings
To debug settings issues:
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
index 896b362735..6af0d636ac 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
@@ -112,7 +112,7 @@ private:
return {};
}
- static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
+ static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true)
{
try
{
@@ -122,6 +122,7 @@ private:
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
+ hotkey.isShown = isShown;
return hotkey;
}
catch (...)
@@ -231,8 +232,10 @@ private:
return false;
}
- void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue)
+ void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true)
{
+ bool actionIsShown = true;
+
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{
return;
@@ -240,9 +243,9 @@ private:
const auto action = actionValue.GetObjectW();
- if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
+ if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown)
{
- return;
+ actionIsShown = false;
}
if (action.HasKey(JSON_KEY_SHORTCUT))
@@ -250,7 +253,7 @@ private:
const AdditionalAction additionalAction
{
actionName.c_str(),
- parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
+ parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
};
m_additional_actions.push_back(additionalAction);
@@ -259,12 +262,12 @@ private:
{
for (const auto& [subActionName, subAction] : action)
{
- process_additional_action(subActionName, subAction);
+ process_additional_action(subActionName, subAction, actionIsShown);
}
}
}
- void read_settings(PowerToysSettings::PowerToyValues& settings)
+ void read_settings(PowerToysSettings::PowerToyValues& settings)
{
const auto settingsObject = settings.get_raw_json();
@@ -317,9 +320,21 @@ private:
{
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
- for (const auto& [actionName, additionalAction] : additionalActions)
+ // Define the expected order to ensure consistent hotkey ID assignment
+ const std::vector expectedOrder = {
+ L"image-to-text",
+ L"paste-as-file",
+ L"transcode"
+ };
+
+ // Process actions in the predefined order
+ for (auto& actionKey : expectedOrder)
{
- process_additional_action(actionName, additionalAction);
+ if (additionalActions.HasKey(actionKey))
+ {
+ const auto actionValue = additionalActions.GetNamedValue(actionKey);
+ process_additional_action(actionKey, actionValue);
+ }
}
}
@@ -331,17 +346,14 @@ private:
for (const auto& customAction : customActions)
{
const auto object = customAction.GetObjectW();
+ bool actionIsShown = object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false);
- if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
- {
- const CustomAction customActionData
- {
- static_cast(object.GetNamedNumber(JSON_KEY_ID)),
- parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
- };
+ const CustomAction customActionData{
+ static_cast(object.GetNamedNumber(JSON_KEY_ID)),
+ parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
+ };
- m_custom_actions.push_back(customActionData);
- }
+ m_custom_actions.push_back(customActionData);
}
}
}
diff --git a/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp b/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp
index 33030fbdfb..29d7a781ae 100644
--- a/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp
+++ b/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp
@@ -556,6 +556,61 @@ public:
return m_enabled;
}
+ virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
+ {
+ constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
+
+ if (hotkeys && buffer_size >= num_hotkeys)
+ {
+ PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
+
+ // Cache the raw JSON object to avoid multiple parsing
+ json::JsonObject root_json = values.get_raw_json();
+ json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
+
+ size_t hotkey_index = 0;
+
+ // Helper lambda to extract hotkey from JSON properties
+ auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey {
+ if (properties_json.HasKey(property_name))
+ {
+ try
+ {
+ json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
+
+ // Extract hotkey properties directly from JSON
+ bool win = hotkey_json.GetNamedBoolean(L"win", false);
+ bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false);
+ bool alt = hotkey_json.GetNamedBoolean(L"alt", false);
+ bool shift = hotkey_json.GetNamedBoolean(L"shift", false);
+ unsigned char key = static_cast(
+ hotkey_json.GetNamedNumber(L"code", 0));
+
+ return { win, ctrl, shift, alt, key };
+ }
+ catch (...)
+ {
+ // If parsing individual hotkey fails, use defaults
+ return { false, false, false, false, 0 };
+ }
+ }
+ else
+ {
+ // Property doesn't exist, use defaults
+ return { false, false, false, false, 0 };
+ }
+ };
+
+ // Extract all hotkeys using the optimized helper
+ hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse
+ hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine
+ hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs
+ hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect
+ }
+
+ return num_hotkeys;
+ }
+
void launch_add_firewall_process()
{
Logger::trace(L"Starting Process to add firewall rule");
diff --git a/src/modules/interface/powertoy_module_interface.h b/src/modules/interface/powertoy_module_interface.h
index b569552659..b88763d1a3 100644
--- a/src/modules/interface/powertoy_module_interface.h
+++ b/src/modules/interface/powertoy_module_interface.h
@@ -45,14 +45,44 @@ public:
bool shift = false;
bool alt = false;
unsigned char key = 0;
+ // The id is used to identify the hotkey in the module. The order in module interface should be the same as in the settings.
+ int id = 0;
+ // Currently, this is only used by AdvancedPaste to determine if the hotkey is shown in the settings.
+ bool isShown = true;
- std::strong_ordering operator<=>(const Hotkey&) const = default;
+ std::strong_ordering operator<=>(const Hotkey& other) const
+ {
+ // Compare bool fields first
+ if (auto cmp = (win <=> other.win); cmp != 0)
+ return cmp;
+ if (auto cmp = (ctrl <=> other.ctrl); cmp != 0)
+ return cmp;
+ if (auto cmp = (shift <=> other.shift); cmp != 0)
+ return cmp;
+ if (auto cmp = (alt <=> other.alt); cmp != 0)
+ return cmp;
+
+ // Compare key value only
+ return key <=> other.key;
+
+ // Note: Deliberately NOT comparing 'name' field
+ }
+
+ bool operator==(const Hotkey& other) const
+ {
+ return win == other.win &&
+ ctrl == other.ctrl &&
+ shift == other.shift &&
+ alt == other.alt &&
+ key == other.key;
+ }
};
struct HotkeyEx
{
WORD modifiersMask = 0;
WORD vkCode = 0;
+ int id = 0;
};
/* Returns the localized name of the PowerToy*/
diff --git a/src/runner/centralized_hotkeys.h b/src/runner/centralized_hotkeys.h
index 29ef079f9e..bb503d332d 100644
--- a/src/runner/centralized_hotkeys.h
+++ b/src/runner/centralized_hotkeys.h
@@ -20,11 +20,13 @@ namespace CentralizedHotkeys
{
WORD modifiersMask;
WORD vkCode;
+ int hotkeyID;
- Shortcut(WORD modifiersMask = 0, WORD vkCode = 0)
+ Shortcut(WORD modifiersMask = 0, WORD vkCode = 0, const int hotkeyID = 0)
{
this->modifiersMask = modifiersMask;
this->vkCode = vkCode;
+ this->hotkeyID = hotkeyID;
}
bool operator<(const Shortcut& key) const
diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp
index 4fdf6b74d2..bb45f7f5ae 100644
--- a/src/runner/general_settings.cpp
+++ b/src/runner/general_settings.cpp
@@ -3,6 +3,7 @@
#include "auto_start_helper.h"
#include "tray_icon.h"
#include "Generated files/resource.h"
+#include "hotkey_conflict_detector.h"
#include
#include "powertoy_module.h"
@@ -204,11 +205,15 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
{
Logger::info(L"apply_general_settings: Enabling powertoy {}", name);
powertoy->enable();
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ hkmng.EnableHotkeyByModule(name);
}
else
{
Logger::info(L"apply_general_settings: Disabling powertoy {}", name);
powertoy->disable();
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ hkmng.DisableHotkeyByModule(name);
}
// Sync the hotkey state with the module state, so it can be removed for disabled modules.
powertoy.UpdateHotkeyEx();
@@ -315,6 +320,8 @@ void start_enabled_powertoys()
{
Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name);
powertoy->enable();
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ hkmng.EnableHotkeyByModule(name);
powertoy.UpdateHotkeyEx();
}
}
diff --git a/src/runner/hotkey_conflict_detector.cpp b/src/runner/hotkey_conflict_detector.cpp
new file mode 100644
index 0000000000..14c8a1ecd9
--- /dev/null
+++ b/src/runner/hotkey_conflict_detector.cpp
@@ -0,0 +1,471 @@
+#include "pch.h"
+#include "hotkey_conflict_detector.h"
+#include
+#include
+#include
+#include
+
+namespace HotkeyConflictDetector
+{
+ Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut)
+ {
+ Hotkey hotkey;
+
+ hotkey.win = (shortcut.modifiersMask & MOD_WIN) != 0;
+ hotkey.ctrl = (shortcut.modifiersMask & MOD_CONTROL) != 0;
+ hotkey.shift = (shortcut.modifiersMask & MOD_SHIFT) != 0;
+ hotkey.alt = (shortcut.modifiersMask & MOD_ALT) != 0;
+
+ hotkey.key = shortcut.vkCode > 255 ? 0 : static_cast(shortcut.vkCode);
+
+ return hotkey;
+ }
+
+ HotkeyConflictManager* HotkeyConflictManager::instance = nullptr;
+ std::mutex HotkeyConflictManager::instanceMutex;
+
+ HotkeyConflictManager& HotkeyConflictManager::GetInstance()
+ {
+ std::lock_guard lock(instanceMutex);
+ if (instance == nullptr)
+ {
+ instance = new HotkeyConflictManager();
+ }
+ return *instance;
+ }
+
+ HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID)
+ {
+ if (disabledHotkeys.find(_moduleName) != disabledHotkeys.end())
+ {
+ return HotkeyConflictType::NoConflict;
+ }
+
+ uint16_t handle = GetHotkeyHandle(_hotkey);
+
+ if (handle == 0)
+ {
+ return HotkeyConflictType::NoConflict;
+ }
+
+ // The order is important, first to check sys conflict and then inapp conflict
+ if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
+ {
+ return HotkeyConflictType::SystemConflict;
+ }
+
+ if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
+ {
+ return HotkeyConflictType::InAppConflict;
+ }
+
+ auto it = hotkeyMap.find(handle);
+
+ if (it == hotkeyMap.end())
+ {
+ return HasConflictWithSystemHotkey(_hotkey) ?
+ HotkeyConflictType::SystemConflict :
+ HotkeyConflictType::NoConflict;
+ }
+
+ if (wcscmp(it->second.moduleName.c_str(), _moduleName) == 0 && it->second.hotkeyID == _hotkeyID)
+ {
+ // A shortcut matching its own assignment is not considered a conflict.
+ return HotkeyConflictType::NoConflict;
+ }
+
+ return HotkeyConflictType::InAppConflict;
+ }
+
+ HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey)
+ {
+ uint16_t handle = GetHotkeyHandle(_hotkey);
+
+ if (handle == 0)
+ {
+ return HotkeyConflictType::NoConflict;
+ }
+
+ // The order is important, first to check sys conflict and then inapp conflict
+ if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
+ {
+ return HotkeyConflictType::SystemConflict;
+ }
+
+ if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
+ {
+ return HotkeyConflictType::InAppConflict;
+ }
+
+ auto it = hotkeyMap.find(handle);
+
+ if (it == hotkeyMap.end())
+ {
+ return HasConflictWithSystemHotkey(_hotkey) ?
+ HotkeyConflictType::SystemConflict :
+ HotkeyConflictType::NoConflict;
+ }
+
+ return HotkeyConflictType::InAppConflict;
+ }
+
+ // This function should only be called when a conflict has already been identified.
+ // It returns a list of all conflicting shortcuts.
+ std::vector HotkeyConflictManager::GetAllConflicts(Hotkey const& _hotkey)
+ {
+ std::vector conflicts;
+ uint16_t handle = GetHotkeyHandle(_hotkey);
+
+ // Check in-app conflicts first
+ auto inAppIt = inAppConflictHotkeyMap.find(handle);
+ if (inAppIt != inAppConflictHotkeyMap.end())
+ {
+ // Add all in-app conflicts
+ for (const auto& conflict : inAppIt->second)
+ {
+ conflicts.push_back(conflict);
+ }
+
+ return conflicts;
+ }
+
+ // Check system conflicts
+ auto sysIt = sysConflictHotkeyMap.find(handle);
+ if (sysIt != sysConflictHotkeyMap.end())
+ {
+ HotkeyConflictInfo systemConflict;
+ systemConflict.hotkey = _hotkey;
+ systemConflict.moduleName = L"System";
+ systemConflict.hotkeyID = 0;
+
+ conflicts.push_back(systemConflict);
+
+ return conflicts;
+ }
+
+ // Check if there's a successfully registered hotkey that would conflict
+ auto registeredIt = hotkeyMap.find(handle);
+ if (registeredIt != hotkeyMap.end())
+ {
+ conflicts.push_back(registeredIt->second);
+
+ return conflicts;
+ }
+
+ // If all the above conditions are ruled out, a system-level conflict is the only remaining explanation.
+ HotkeyConflictInfo systemConflict;
+ systemConflict.hotkey = _hotkey;
+ systemConflict.moduleName = L"System";
+ systemConflict.hotkeyID = 0;
+ conflicts.push_back(systemConflict);
+
+ return conflicts;
+ }
+
+ bool HotkeyConflictManager::AddHotkey(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID, bool isEnabled)
+ {
+ if (!isEnabled)
+ {
+ disabledHotkeys[_moduleName].push_back({ _hotkey, _moduleName, _hotkeyID });
+ return true;
+ }
+
+ uint16_t handle = GetHotkeyHandle(_hotkey);
+
+ if (handle == 0)
+ {
+ return false;
+ }
+
+ HotkeyConflictType conflictType = HasConflict(_hotkey, _moduleName, _hotkeyID);
+ if (conflictType != HotkeyConflictType::NoConflict)
+ {
+ if (conflictType == HotkeyConflictType::InAppConflict)
+ {
+ auto hotkeyFound = hotkeyMap.find(handle);
+ inAppConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID });
+
+ if (hotkeyFound != hotkeyMap.end())
+ {
+ inAppConflictHotkeyMap[handle].insert(hotkeyFound->second);
+ hotkeyMap.erase(hotkeyFound);
+ }
+ }
+ else
+ {
+ sysConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID });
+ }
+ return false;
+ }
+
+ HotkeyConflictInfo hotkeyInfo;
+ hotkeyInfo.moduleName = _moduleName;
+ hotkeyInfo.hotkeyID = _hotkeyID;
+ hotkeyInfo.hotkey = _hotkey;
+ hotkeyMap[handle] = hotkeyInfo;
+
+ return true;
+ }
+
+ std::vector HotkeyConflictManager::RemoveHotkeyByModule(const std::wstring& moduleName)
+ {
+ std::vector removedHotkeys;
+
+ if (disabledHotkeys.find(moduleName) != disabledHotkeys.end())
+ {
+ disabledHotkeys.erase(moduleName);
+ }
+
+ std::lock_guard lock(hotkeyMutex);
+ bool foundRecord = false;
+
+ for (auto it = sysConflictHotkeyMap.begin(); it != sysConflictHotkeyMap.end();)
+ {
+ auto& conflictSet = it->second;
+ for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
+ {
+ if (setIt->moduleName == moduleName)
+ {
+ removedHotkeys.push_back(*setIt);
+ setIt = conflictSet.erase(setIt);
+ foundRecord = true;
+ }
+ else
+ {
+ ++setIt;
+ }
+ }
+ if (conflictSet.empty())
+ {
+ it = sysConflictHotkeyMap.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ for (auto it = inAppConflictHotkeyMap.begin(); it != inAppConflictHotkeyMap.end();)
+ {
+ auto& conflictSet = it->second;
+ uint16_t handle = it->first;
+
+ for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
+ {
+ if (setIt->moduleName == moduleName)
+ {
+ removedHotkeys.push_back(*setIt);
+ setIt = conflictSet.erase(setIt);
+ foundRecord = true;
+ }
+ else
+ {
+ ++setIt;
+ }
+ }
+
+ if (conflictSet.empty())
+ {
+ it = inAppConflictHotkeyMap.erase(it);
+ }
+ else if (conflictSet.size() == 1)
+ {
+ // Move the only remaining conflict to main map
+ const auto& onlyConflict = *conflictSet.begin();
+ hotkeyMap[handle] = onlyConflict;
+ it = inAppConflictHotkeyMap.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ for (auto it = hotkeyMap.begin(); it != hotkeyMap.end();)
+ {
+ if (it->second.moduleName == moduleName)
+ {
+ uint16_t handle = it->first;
+ removedHotkeys.push_back(it->second);
+ it = hotkeyMap.erase(it);
+ foundRecord = true;
+
+ auto inAppIt = inAppConflictHotkeyMap.find(handle);
+ if (inAppIt != inAppConflictHotkeyMap.end() && inAppIt->second.size() == 1)
+ {
+ // Move the only in-app conflict to main map
+ const auto& onlyConflict = *inAppIt->second.begin();
+ hotkeyMap[handle] = onlyConflict;
+ inAppConflictHotkeyMap.erase(inAppIt);
+ }
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ return removedHotkeys;
+ }
+
+ void HotkeyConflictManager::EnableHotkeyByModule(const std::wstring& moduleName)
+ {
+ if (disabledHotkeys.find(moduleName) == disabledHotkeys.end())
+ {
+ return; // No disabled hotkeys for this module
+ }
+
+ auto hotkeys = disabledHotkeys[moduleName];
+ disabledHotkeys.erase(moduleName);
+
+ for (const auto& hotkeyInfo : hotkeys)
+ {
+ // Re-add the hotkey as enabled
+ AddHotkey(hotkeyInfo.hotkey, moduleName.c_str(), hotkeyInfo.hotkeyID, true);
+ }
+ }
+
+ void HotkeyConflictManager::DisableHotkeyByModule(const std::wstring& moduleName)
+ {
+ auto hotkeys = RemoveHotkeyByModule(moduleName);
+ disabledHotkeys[moduleName] = hotkeys;
+ }
+
+ bool HotkeyConflictManager::HasConflictWithSystemHotkey(const Hotkey& hotkey)
+ {
+ // Convert PowerToys Hotkey format to Win32 RegisterHotKey format
+ UINT modifiers = 0;
+ if (hotkey.win)
+ {
+ modifiers |= MOD_WIN;
+ }
+ if (hotkey.ctrl)
+ {
+ modifiers |= MOD_CONTROL;
+ }
+ if (hotkey.alt)
+ {
+ modifiers |= MOD_ALT;
+ }
+ if (hotkey.shift)
+ {
+ modifiers |= MOD_SHIFT;
+ }
+
+ // No modifiers or no key is not a valid hotkey
+ if (modifiers == 0 || hotkey.key == 0)
+ {
+ return false;
+ }
+
+ // Use a unique ID for this test registration
+ const int hotkeyId = 0x0FFF; // Arbitrary ID for temporary registration
+
+ // Try to register the hotkey with Windows, using nullptr instead of a window handle
+ if (!RegisterHotKey(nullptr, hotkeyId, modifiers, hotkey.key))
+ {
+ // If registration fails with ERROR_HOTKEY_ALREADY_REGISTERED, it means the hotkey
+ // is already in use by the system or another application
+ if (GetLastError() == ERROR_HOTKEY_ALREADY_REGISTERED)
+ {
+ return true;
+ }
+ }
+ else
+ {
+ // If registration succeeds, unregister it immediately
+ UnregisterHotKey(nullptr, hotkeyId);
+ }
+
+ return false;
+ }
+
+ json::JsonObject HotkeyConflictManager::GetHotkeyConflictsAsJson()
+ {
+ std::lock_guard lock(hotkeyMutex);
+
+ using namespace json;
+ JsonObject root;
+
+ // Serialize hotkey to a unique string format for grouping
+ auto serializeHotkey = [](const Hotkey& hotkey) -> JsonObject {
+ JsonObject obj;
+ obj.Insert(L"win", value(hotkey.win));
+ obj.Insert(L"ctrl", value(hotkey.ctrl));
+ obj.Insert(L"shift", value(hotkey.shift));
+ obj.Insert(L"alt", value(hotkey.alt));
+ obj.Insert(L"key", value(static_cast(hotkey.key)));
+ return obj;
+ };
+
+ // New format: Group conflicts by hotkey
+ JsonArray inAppConflictsArray;
+ JsonArray sysConflictsArray;
+
+ // Process in-app conflicts - only include hotkeys that are actually in conflict
+ for (const auto& [handle, conflicts] : inAppConflictHotkeyMap)
+ {
+ if (!conflicts.empty())
+ {
+ JsonObject conflictGroup;
+
+ // All entries have the same hotkey, so use the first one for the key
+ conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
+
+ // Create an array of module info without repeating the hotkey
+ JsonArray modules;
+ for (const auto& info : conflicts)
+ {
+ JsonObject moduleInfo;
+ moduleInfo.Insert(L"moduleName", value(info.moduleName));
+ moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID));
+ modules.Append(moduleInfo);
+ }
+
+ conflictGroup.Insert(L"modules", modules);
+ inAppConflictsArray.Append(conflictGroup);
+ }
+ }
+
+ // Process system conflicts - only include hotkeys that are actually in conflict
+ for (const auto& [handle, conflicts] : sysConflictHotkeyMap)
+ {
+ if (!conflicts.empty())
+ {
+ JsonObject conflictGroup;
+
+ // All entries have the same hotkey, so use the first one for the key
+ conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
+
+ // Create an array of module info without repeating the hotkey
+ JsonArray modules;
+ for (const auto& info : conflicts)
+ {
+ JsonObject moduleInfo;
+ moduleInfo.Insert(L"moduleName", value(info.moduleName));
+ moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID));
+ modules.Append(moduleInfo);
+ }
+
+ conflictGroup.Insert(L"modules", modules);
+ sysConflictsArray.Append(conflictGroup);
+ }
+ }
+
+ // Add the grouped conflicts to the root object
+ root.Insert(L"inAppConflicts", inAppConflictsArray);
+ root.Insert(L"sysConflicts", sysConflictsArray);
+
+ return root;
+ }
+
+ uint16_t HotkeyConflictManager::GetHotkeyHandle(const Hotkey& hotkey)
+ {
+ uint16_t handle = hotkey.key;
+ handle |= hotkey.win << 8;
+ handle |= hotkey.ctrl << 9;
+ handle |= hotkey.shift << 10;
+ handle |= hotkey.alt << 11;
+ return handle;
+ }
+}
\ No newline at end of file
diff --git a/src/runner/hotkey_conflict_detector.h b/src/runner/hotkey_conflict_detector.h
new file mode 100644
index 0000000000..c32954e3e4
--- /dev/null
+++ b/src/runner/hotkey_conflict_detector.h
@@ -0,0 +1,100 @@
+#pragma once
+#include "pch.h"
+#include
+#include
+#include
+
+#include "../modules/interface/powertoy_module_interface.h"
+#include "centralized_hotkeys.h"
+#include "common/utils/json.h"
+
+namespace HotkeyConflictDetector
+{
+ using Hotkey = PowertoyModuleIface::Hotkey;
+ using HotkeyEx = PowertoyModuleIface::HotkeyEx;
+ using Shortcut = CentralizedHotkeys::Shortcut;
+
+ struct HotkeyConflictInfo
+ {
+ Hotkey hotkey;
+ std::wstring moduleName;
+ int hotkeyID = 0;
+
+ inline bool operator==(const HotkeyConflictInfo& other) const
+ {
+ return hotkey == other.hotkey &&
+ moduleName == other.moduleName &&
+ hotkeyID == other.hotkeyID;
+ }
+ };
+
+ Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut);
+
+ enum HotkeyConflictType
+ {
+ NoConflict = 0,
+ SystemConflict = 1,
+ InAppConflict = 2,
+ };
+
+ class HotkeyConflictManager
+ {
+ public:
+ static HotkeyConflictManager& GetInstance();
+
+ HotkeyConflictType HasConflict(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID);
+ HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey);
+ std::vector HotkeyConflictManager::GetAllConflicts(Hotkey const& hotkey);
+ bool AddHotkey(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID, bool isEnabled);
+ std::vector RemoveHotkeyByModule(const std::wstring& moduleName);
+
+ void EnableHotkeyByModule(const std::wstring& moduleName);
+ void DisableHotkeyByModule(const std::wstring& moduleName);
+
+ json::JsonObject GetHotkeyConflictsAsJson();
+
+ private:
+ static std::mutex instanceMutex;
+ static HotkeyConflictManager* instance;
+
+ std::mutex hotkeyMutex;
+ // Hotkey in hotkeyMap means the hotkey has been registered successfully
+ std::unordered_map hotkeyMap;
+ // Hotkey in sysConflictHotkeyMap means the hotkey has conflict with system defined hotkeys
+ std::unordered_map> sysConflictHotkeyMap;
+ // Hotkey in inAppConflictHotkeyMap means the hotkey has conflict with other modules
+ std::unordered_map> inAppConflictHotkeyMap;
+
+ std::unordered_map> disabledHotkeys;
+
+ uint16_t GetHotkeyHandle(const Hotkey&);
+ bool HasConflictWithSystemHotkey(const Hotkey&);
+
+ HotkeyConflictManager() = default;
+ };
+};
+
+namespace std
+{
+ template<>
+ struct hash
+ {
+ size_t operator()(const HotkeyConflictDetector::HotkeyConflictInfo& info) const
+ {
+
+ size_t hotkeyHash =
+ (info.hotkey.win ? 1ULL : 0ULL) |
+ ((info.hotkey.ctrl ? 1ULL : 0ULL) << 1) |
+ ((info.hotkey.shift ? 1ULL : 0ULL) << 2) |
+ ((info.hotkey.alt ? 1ULL : 0ULL) << 3) |
+ (static_cast(info.hotkey.key) << 4);
+
+ size_t moduleHash = std::hash{}(info.moduleName);
+ size_t idHash = std::hash{}(info.hotkeyID);
+
+ return hotkeyHash ^
+ ((moduleHash << 1) | (moduleHash >> (sizeof(size_t) * 8 - 1))) ^ // rotate left 1 bit
+ ((idHash << 2) | (idHash >> (sizeof(size_t) * 8 - 2))); // rotate left 2 bits
+ }
+ };
+}
diff --git a/src/runner/powertoy_module.cpp b/src/runner/powertoy_module.cpp
index 32f856f465..eb1f7c4fd7 100644
--- a/src/runner/powertoy_module.cpp
+++ b/src/runner/powertoy_module.cpp
@@ -40,13 +40,14 @@ json::JsonObject PowertoyModule::json_config() const
}
PowertoyModule::PowertoyModule(PowertoyModuleIface* pt_module, HMODULE handle) :
- handle(handle), pt_module(pt_module)
+ handle(handle), pt_module(pt_module), hkmng(HotkeyConflictDetector::HotkeyConflictManager::GetInstance())
{
if (!pt_module)
{
throw std::runtime_error("Module not initialized");
}
+ remove_hotkey_records();
update_hotkeys();
UpdateHotkeyEx();
}
@@ -63,19 +64,27 @@ void PowertoyModule::update_hotkeys()
for (size_t i = 0; i < hotkeyCount; i++)
{
- CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] {
- Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key());
- return modulePtr->on_hotkey(i);
- });
+ if (hotkeys[i].isShown)
+ {
+ hkmng.AddHotkey(hotkeys[i], pt_module->get_key(), static_cast(i), pt_module->is_enabled());
+
+ CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] {
+ Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key());
+ return modulePtr->on_hotkey(i);
+ });
+ }
}
}
void PowertoyModule::UpdateHotkeyEx()
{
CentralizedHotkeys::UnregisterHotkeysForModule(pt_module->get_key());
+
auto container = pt_module->GetHotkeyEx();
if (container.has_value() && pt_module->is_enabled())
{
+ hkmng.RemoveHotkeyByModule(pt_module->get_key());
+
auto hotkey = container.value();
auto modulePtr = pt_module.get();
auto action = [modulePtr](WORD /*modifiersMask*/, WORD /*vkCode*/) {
@@ -83,6 +92,9 @@ void PowertoyModule::UpdateHotkeyEx()
modulePtr->OnHotkeyEx();
};
+ HotkeyConflictDetector::Hotkey _hotkey = HotkeyConflictDetector::ShortcutToHotkey({ hotkey.modifiersMask, hotkey.vkCode });
+ hkmng.AddHotkey(_hotkey, pt_module->get_key(), 0, pt_module->is_enabled()); // This is the only one activation hotkey, so we use "0" as the name.
+
CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode }, { pt_module->get_key(), action });
}
diff --git a/src/runner/powertoy_module.h b/src/runner/powertoy_module.h
index 9332e5f025..9b7a9a59bd 100644
--- a/src/runner/powertoy_module.h
+++ b/src/runner/powertoy_module.h
@@ -5,6 +5,7 @@
#include
#include
#include
+#include "hotkey_conflict_detector.h"
#include
@@ -44,9 +45,17 @@ public:
void UpdateHotkeyEx();
+ inline void remove_hotkey_records()
+ {
+ hkmng.RemoveHotkeyByModule(pt_module->get_key());
+ }
+
private:
+ HotkeyConflictDetector::HotkeyConflictManager& hkmng;
std::unique_ptr handle;
std::unique_ptr pt_module;
+
+
};
PowertoyModule load_powertoy(const std::wstring_view filename);
diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj
index a55396a71a..90dafb5e45 100644
--- a/src/runner/runner.vcxproj
+++ b/src/runner/runner.vcxproj
@@ -51,6 +51,7 @@
+
Create
@@ -71,6 +72,7 @@
+
diff --git a/src/runner/runner.vcxproj.filters b/src/runner/runner.vcxproj.filters
index a91782fd24..812d7857a2 100644
--- a/src/runner/runner.vcxproj.filters
+++ b/src/runner/runner.vcxproj.filters
@@ -45,6 +45,9 @@
Utils
+
+ Utils
+
@@ -93,6 +96,9 @@
Utils
+
+ Utils
+
diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp
index e811ff5d65..b3ced3b858 100644
--- a/src/runner/settings_window.cpp
+++ b/src/runner/settings_window.cpp
@@ -13,6 +13,7 @@
#include "UpdateUtils.h"
#include "centralized_kb_hook.h"
#include "Generated files/resource.h"
+#include "hotkey_conflict_detector.h"
#include
#include
@@ -153,6 +154,8 @@ void send_json_config_to_module(const std::wstring& module_key, const std::wstri
if (moduleIt != modules().end())
{
moduleIt->second->set_config(settings.c_str());
+
+ moduleIt->second.remove_hotkey_records();
moduleIt->second.update_hotkeys();
moduleIt->second.UpdateHotkeyEx();
}
@@ -249,6 +252,77 @@ void dispatch_received_json(const std::wstring& json_to_parse)
const std::wstring save_file_location = PTSettingsHelper::get_root_save_folder_location() + language_filename;
json::to_file(save_file_location, j);
}
+ else if (name == L"check_hotkey_conflict")
+ {
+ try
+ {
+ PowertoyModuleIface::Hotkey hotkey;
+ hotkey.win = value.GetObjectW().GetNamedBoolean(L"win", false);
+ hotkey.ctrl = value.GetObjectW().GetNamedBoolean(L"ctrl", false);
+ hotkey.shift = value.GetObjectW().GetNamedBoolean(L"shift", false);
+ hotkey.alt = value.GetObjectW().GetNamedBoolean(L"alt", false);
+ hotkey.key = static_cast(value.GetObjectW().GetNamedNumber(L"key", 0));
+
+ std::wstring requestId = value.GetObjectW().GetNamedString(L"request_id", L"").c_str();
+
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ bool hasConflict = hkmng.HasConflict(hotkey);
+
+ json::JsonObject response;
+ response.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"hotkey_conflict_result"));
+ response.SetNamedValue(L"request_id", json::JsonValue::CreateStringValue(requestId));
+ response.SetNamedValue(L"has_conflict", json::JsonValue::CreateBooleanValue(hasConflict));
+
+ if (hasConflict)
+ {
+ auto conflicts = hkmng.GetAllConflicts(hotkey);
+ if (!conflicts.empty())
+ {
+ // Include all conflicts in the response
+ json::JsonArray allConflicts;
+ for (const auto& conflict : conflicts)
+ {
+ json::JsonObject conflictObj;
+ conflictObj.SetNamedValue(L"module", json::JsonValue::CreateStringValue(conflict.moduleName));
+ conflictObj.SetNamedValue(L"hotkeyID", json::JsonValue::CreateNumberValue(conflict.hotkeyID));
+ allConflicts.Append(conflictObj);
+ }
+ response.SetNamedValue(L"all_conflicts", allConflicts);
+ }
+ }
+
+ std::unique_lock lock{ ipc_mutex };
+ if (current_settings_ipc)
+ {
+ current_settings_ipc->send(response.Stringify().c_str());
+ }
+ }
+ catch (...)
+ {
+ Logger::error(L"Failed to process hotkey conflict check request");
+ }
+ }
+ else if (name == L"get_all_hotkey_conflicts")
+ {
+ try
+ {
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ auto conflictsJson = hkmng.GetHotkeyConflictsAsJson();
+
+ // Add response type identifier
+ conflictsJson.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"all_hotkey_conflicts"));
+
+ std::unique_lock lock{ ipc_mutex };
+ if (current_settings_ipc)
+ {
+ current_settings_ipc->send(conflictsJson.Stringify().c_str());
+ }
+ }
+ catch (...)
+ {
+ Logger::error(L"Failed to process get all hotkey conflicts request");
+ }
+ }
}
return;
}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
index 28bed92012..1642ecf9c4 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
@@ -12,7 +12,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction
{
private HotkeySettings _shortcut = new();
- private bool _isShown = true;
+ private bool _isShown;
+ private bool _hasConflict;
+ private string _tooltip;
[JsonPropertyName("shortcut")]
public HotkeySettings Shortcut
@@ -38,6 +40,20 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
set => Set(ref _isShown, value);
}
+ [JsonIgnore]
+ public bool HasConflict
+ {
+ get => _hasConflict;
+ set => Set(ref _hasConflict, value);
+ }
+
+ [JsonIgnore]
+ public string Tooltip
+ {
+ get => _tooltip;
+ set => Set(ref _tooltip, value);
+ }
+
[JsonIgnore]
public IEnumerable SubActions => [];
}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
index 3b1a859364..6d908c617a 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
@@ -28,16 +28,23 @@ public sealed class AdvancedPasteAdditionalActions
public IEnumerable GetAllActions()
{
- Queue queue = new([ImageToText, PasteAsFile, Transcode]);
+ return GetAllActionsRecursive([ImageToText, PasteAsFile, Transcode]);
+ }
- while (queue.Count != 0)
+ ///
+ /// Changed to depth-first traversal to ensure ordered output
+ ///
+ /// The collection of actions to traverse
+ /// All actions returned in depth-first order
+ private static IEnumerable GetAllActionsRecursive(IEnumerable actions)
+ {
+ foreach (var action in actions)
{
- var action = queue.Dequeue();
yield return action;
- foreach (var subAction in action.SubActions)
+ foreach (var subAction in GetAllActionsRecursive(action.SubActions))
{
- queue.Enqueue(subAction);
+ yield return subAction;
}
}
}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
index 971d24c93b..43baf89351 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
@@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Library;
@@ -20,6 +20,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private bool _canMoveUp;
private bool _canMoveDown;
private bool _isValid;
+ private bool _hasConflict;
+ private string _tooltip;
[JsonPropertyName("id")]
public int Id
@@ -65,7 +67,6 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
// We null-coalesce here rather than outside this branch as we want to raise PropertyChanged when the setter is called
// with null; the ShortcutControl depends on this.
_shortcut = value ?? new();
-
OnPropertyChanged();
}
}
@@ -99,6 +100,20 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private set => Set(ref _isValid, value);
}
+ [JsonIgnore]
+ public bool HasConflict
+ {
+ get => _hasConflict;
+ set => Set(ref _hasConflict, value);
+ }
+
+ [JsonIgnore]
+ public string Tooltip
+ {
+ get => _tooltip;
+ set => Set(ref _tooltip, value);
+ }
+
[JsonIgnore]
public IEnumerable SubActions => [];
@@ -118,6 +133,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
IsShown = other.IsShown;
CanMoveUp = other.CanMoveUp;
CanMoveDown = other.CanMoveDown;
+ HasConflict = other.HasConflict;
+ Tooltip = other.Tooltip;
}
private HotkeySettings GetShortcutClone()
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
index e3ba7d4122..ca9cdacff6 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig
+ public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "AdvancedPaste";
@@ -39,6 +41,64 @@ namespace Microsoft.PowerToys.Settings.UI.Library
settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName);
}
+ public ModuleType GetModuleType() => ModuleType.AdvancedPaste;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.PasteAsPlainTextShortcut,
+ value => Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut,
+ "PasteAsPlainText_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.AdvancedPasteUIShortcut,
+ value => Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut,
+ "AdvancedPasteUI_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.PasteAsMarkdownShortcut,
+ value => Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings(),
+ "PasteAsMarkdown_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.PasteAsJsonShortcut,
+ value => Properties.PasteAsJsonShortcut = value ?? new HotkeySettings(),
+ "PasteAsJson_Shortcut"),
+ };
+
+ string[] additionalActionHeaderKeys =
+ [
+ "ImageToText",
+ "PasteAsTxtFile",
+ "PasteAsPngFile",
+ "PasteAsHtmlFile",
+ "TranscodeToMp3",
+ "TranscodeToMp4",
+ ];
+ int index = 0;
+ foreach (var action in Properties.AdditionalActions.GetAllActions())
+ {
+ if (action is AdvancedPasteAdditionalAction additionalAction)
+ {
+ hotkeyAccessors.Add(new HotkeyAccessor(
+ () => additionalAction.Shortcut,
+ value => additionalAction.Shortcut = value ?? new HotkeySettings(),
+ additionalActionHeaderKeys[index]));
+ index++;
+ }
+ }
+
+ // Custom actions do not have localization header, just use the action name.
+ foreach (var customAction in Properties.CustomActions.Value)
+ {
+ hotkeyAccessors.Add(new HotkeyAccessor(
+ () => customAction.Shortcut,
+ value => customAction.Shortcut = value ?? new HotkeySettings(),
+ customAction.Name));
+ }
+
+ return hotkeyAccessors.ToArray();
+ }
+
public string GetModuleName()
=> Name;
diff --git a/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs b/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs
index 449c1c0a76..cb7e138596 100644
--- a/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class AlwaysOnTopSettings : BasePTModuleSettings, ISettingsConfig
+ public class AlwaysOnTopSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "AlwaysOnTop";
public const string ModuleVersion = "0.0.1";
@@ -28,6 +30,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.AlwaysOnTop;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.Hotkey.Value,
+ value => Properties.Hotkey.Value = value ?? AlwaysOnTopProperties.DefaultHotkeyValue,
+ "AlwaysOnTop_ActivationShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public bool UpgradeSettingsConfiguration()
{
return false;
diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs b/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs
index 641625e180..b601b75baa 100644
--- a/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs
@@ -7,14 +7,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
-
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class ColorPickerSettings : BasePTModuleSettings, ISettingsConfig
+ public class ColorPickerSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "ColorPicker";
@@ -64,6 +64,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return false;
}
+ public ModuleType GetModuleType() => ModuleType.ColorPicker;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "Activation_Shortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public static object UpgradeSettings(object oldSettingsObject)
{
ColorPickerSettingsVersion1 oldSettings = (ColorPickerSettingsVersion1)oldSettingsObject;
diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs b/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs
index 840788992d..1ddff1946f 100644
--- a/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs
+++ b/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs
@@ -5,7 +5,6 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
-
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs
index ed6600f287..517c4e8754 100644
--- a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class CropAndLockSettings : BasePTModuleSettings, ISettingsConfig
+ public class CropAndLockSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "CropAndLock";
public const string ModuleVersion = "0.0.1";
@@ -28,6 +30,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.CropAndLock;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ReparentHotkey.Value,
+ value => Properties.ReparentHotkey.Value = value ?? CropAndLockProperties.DefaultReparentHotkeyValue,
+ "CropAndLock_ReparentActivation_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.ThumbnailHotkey.Value,
+ value => Properties.ThumbnailHotkey.Value = value ?? CropAndLockProperties.DefaultThumbnailHotkeyValue,
+ "CropAndLock_ThumbnailActivation_Shortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public bool UpgradeSettingsConfiguration()
{
return false;
diff --git a/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs b/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs
index aca45d0b01..fb00351ee2 100644
--- a/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig
+ public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "FindMyMouse";
@@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.FindMyMouse;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "MouseUtils_FindMyMouse_ActivationShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
diff --git a/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs b/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs
new file mode 100644
index 0000000000..41c4d4af61
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
+{
+ public class HotkeyAccessor
+ {
+ public Func Getter { get; }
+
+ public Action Setter { get; }
+
+ public HotkeyAccessor(Func getter, Action setter, string localizationHeaderKey = "")
+ {
+ Getter = getter ?? throw new ArgumentNullException(nameof(getter));
+ Setter = setter ?? throw new ArgumentNullException(nameof(setter));
+ LocalizationHeaderKey = localizationHeaderKey;
+ }
+
+ public HotkeySettings Value
+ {
+ get => Getter();
+ set => Setter(value);
+ }
+
+ public string LocalizationHeaderKey { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs
new file mode 100644
index 0000000000..00d2145f29
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class AllHotkeyConflictsData
+ {
+ public List InAppConflicts { get; set; } = new List();
+
+ public List SystemConflicts { get; set; } = new List();
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs
new file mode 100644
index 0000000000..28f034d81b
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class AllHotkeyConflictsEventArgs : EventArgs
+ {
+ public AllHotkeyConflictsData Conflicts { get; }
+
+ public AllHotkeyConflictsEventArgs(AllHotkeyConflictsData conflicts)
+ {
+ Conflicts = conflicts;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs
new file mode 100644
index 0000000000..a420ec7a2b
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class HotkeyConflictGroupData
+ {
+ public HotkeyData Hotkey { get; set; }
+
+ public bool IsSystemConflict { get; set; }
+
+ public List Modules { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs
new file mode 100644
index 0000000000..193eb39d89
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class HotkeyConflictInfo
+ {
+ public bool IsSystemConflict { get; set; }
+
+ public string ConflictingModuleName { get; set; }
+
+ public int ConflictingHotkeyID { get; set; }
+
+ public List AllConflictingModules { get; set; } = new List();
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs
new file mode 100644
index 0000000000..9e416db7d9
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.Settings.UI.Library.Utilities;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class HotkeyData
+ {
+ public bool Win { get; set; }
+
+ public bool Ctrl { get; set; }
+
+ public bool Shift { get; set; }
+
+ public bool Alt { get; set; }
+
+ public int Key { get; set; }
+
+ public List