[Feature] PowerToys hotkey conflict detection (#41029)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## 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

![image](https://github.com/user-attachments/assets/3915174e-d1e7-4f86-8835-2a1bafcc85c9)

<details>
<summary>Demo videos</summary>


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
</details>

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually validation performed.

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Shawn Yuan
2025-08-20 09:31:52 +08:00
committed by GitHub
parent ce4d8dc11e
commit 75526b9580
104 changed files with 4578 additions and 366 deletions

View File

@@ -25,6 +25,8 @@ ADMINS
adml adml
admx admx
advancedpaste advancedpaste
advancedpasteui
advancedpasteuishortcut
advfirewall advfirewall
AFeature AFeature
affordances affordances
@@ -40,6 +42,7 @@ ALLINPUT
Allman Allman
Allmodule Allmodule
ALLOWUNDO ALLOWUNDO
allpc
ALLVIEW ALLVIEW
ALPHATYPE ALPHATYPE
AModifier AModifier
@@ -629,6 +632,7 @@ HKCU
hkey hkey
HKLM HKLM
HKM HKM
hkmng
HKPD HKPD
HKU HKU
HMD HMD
@@ -646,7 +650,11 @@ Hostx
hotfixes hotfixes
hotkeycontrol hotkeycontrol
HOTKEYF HOTKEYF
hotkeylockmachine
hotkeyreconnect
hotkeys hotkeys
hotkeyswitch
hotkeytoggleeasymouse
hotlight hotlight
hotspot hotspot
HPAINTBUFFER HPAINTBUFFER
@@ -704,9 +712,12 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT IMAGERESIZEREXT
imageresizerinput imageresizerinput
imageresizersettings imageresizersettings
imagetotext
imagetotextshortcut
imagingdevices imagingdevices
ime ime
imgflip imgflip
inapp
inbox inbox
INCONTACT INCONTACT
Indo Indo
@@ -789,6 +800,7 @@ keyvault
KILLFOCUS KILLFOCUS
killrunner killrunner
kmph kmph
kvp
Kybd Kybd
lastcodeanalysissucceeded lastcodeanalysissucceeded
LASTEXITCODE LASTEXITCODE
@@ -827,6 +839,7 @@ localappdata
localpackage localpackage
LOCALSYSTEM LOCALSYSTEM
LOCATIONCHANGE LOCATIONCHANGE
LOCKMACHINE
LOCKTYPE LOCKTYPE
LOGFONT LOGFONT
LOGFONTW LOGFONTW
@@ -912,6 +925,7 @@ MDL
mdtext mdtext
mdtxt mdtxt
mdwn mdwn
measuretool
meme meme
memicmp memicmp
MENUITEMINFO MENUITEMINFO
@@ -961,6 +975,7 @@ MOUSEHWHEEL
MOUSEINPUT MOUSEINPUT
mousejump mousejump
mousepointer mousepointer
mousepointercrosshairs
mouseutils mouseutils
MOVESIZEEND MOVESIZEEND
MOVESIZESTART MOVESIZESTART
@@ -1161,6 +1176,18 @@ PARENTRELATIVEFORADDRESSBAR
PARENTRELATIVEPARSING PARENTRELATIVEPARSING
parray parray
PARTIALCONFIRMATIONDIALOGTITLE PARTIALCONFIRMATIONDIALOGTITLE
pasteashtmlfile
pasteashtmlfileshortcut
pasteasjson
pasteasjsonshortcut
pasteasmarkdown
pasteasmarkdownshortcut
pasteasplaintext
pasteasplaintextshortcut
pasteaspngfile
pasteaspngfileshortcut
pasteastxtfile
pasteastxtfileshortcut
PATCOPY PATCOPY
PATHMUSTEXIST PATHMUSTEXIST
PATINVERT PATINVERT
@@ -1228,6 +1255,7 @@ Pomodoro
Popups Popups
POPUPWINDOW POPUPWINDOW
POSITIONITEM POSITIONITEM
powerocr
POWERRENAMECONTEXTMENU POWERRENAMECONTEXTMENU
powerrenameinput powerrenameinput
POWERRENAMETEST POWERRENAMETEST
@@ -1368,6 +1396,7 @@ Removelnk
renamable renamable
RENAMEONCOLLISION RENAMEONCOLLISION
reparented reparented
reparenthotkey
reparenting reparenting
reportfileaccesses reportfileaccesses
requery requery
@@ -1687,6 +1716,7 @@ THH
THICKFRAME THICKFRAME
THISCOMPONENT THISCOMPONENT
throughs throughs
thumbnailhotkey
TILEDWINDOW TILEDWINDOW
TILLSON TILLSON
timedate timedate
@@ -1701,6 +1731,7 @@ tlb
tlbimp tlbimp
tlc tlc
TNP TNP
TOGGLEEASYMOUSE
Toolhelp Toolhelp
toolkitconverters toolkitconverters
toolwindow toolwindow
@@ -1714,6 +1745,7 @@ tracelogging
tracerpt tracerpt
trackbar trackbar
trafficmanager trafficmanager
transcodetomp
transicc transicc
TRAYMOUSEMESSAGE TRAYMOUSEMESSAGE
triaging triaging

View File

@@ -71,6 +71,41 @@ When the user changes settings in the UI:
3. The runner calls the `set_config` function on the appropriate module 3. The runner calls the `set_config` function on the appropriate module
4. The module parses the JSON and applies the new settings 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<HotkeyEx> 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 modules 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 modules ViewModel
The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary<string, HotkeySettings[]> 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 modules Views call `OnPageLoaded()`
Once the modules view is loaded, make sure to invoke the ViewModels `OnPageLoaded()` method:
```cs
Loaded += (s, e) => ViewModel.OnPageLoaded();
```
- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
## Debugging Settings ## Debugging Settings
To debug settings issues: To debug settings issues:

View File

@@ -112,7 +112,7 @@ private:
return {}; 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 try
{ {
@@ -122,6 +122,7 @@ private:
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
hotkey.isShown = isShown;
return hotkey; return hotkey;
} }
catch (...) catch (...)
@@ -231,8 +232,10 @@ private:
return false; 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) if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{ {
return; return;
@@ -240,9 +243,9 @@ private:
const auto action = actionValue.GetObjectW(); 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)) if (action.HasKey(JSON_KEY_SHORTCUT))
@@ -250,7 +253,7 @@ private:
const AdditionalAction additionalAction const AdditionalAction additionalAction
{ {
actionName.c_str(), 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); m_additional_actions.push_back(additionalAction);
@@ -259,12 +262,12 @@ private:
{ {
for (const auto& [subActionName, subAction] : action) 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(); const auto settingsObject = settings.get_raw_json();
@@ -317,9 +320,21 @@ private:
{ {
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS); 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<winrt::hstring> 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) for (const auto& customAction : customActions)
{ {
const auto object = customAction.GetObjectW(); 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<int>(object.GetNamedNumber(JSON_KEY_ID)),
const CustomAction customActionData parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
{ };
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
};
m_custom_actions.push_back(customActionData); m_custom_actions.push_back(customActionData);
}
} }
} }
} }

View File

@@ -556,6 +556,61 @@ public:
return m_enabled; 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<unsigned char>(
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() void launch_add_firewall_process()
{ {
Logger::trace(L"Starting Process to add firewall rule"); Logger::trace(L"Starting Process to add firewall rule");

View File

@@ -45,14 +45,44 @@ public:
bool shift = false; bool shift = false;
bool alt = false; bool alt = false;
unsigned char key = 0; 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 struct HotkeyEx
{ {
WORD modifiersMask = 0; WORD modifiersMask = 0;
WORD vkCode = 0; WORD vkCode = 0;
int id = 0;
}; };
/* Returns the localized name of the PowerToy*/ /* Returns the localized name of the PowerToy*/

View File

@@ -20,11 +20,13 @@ namespace CentralizedHotkeys
{ {
WORD modifiersMask; WORD modifiersMask;
WORD vkCode; 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->modifiersMask = modifiersMask;
this->vkCode = vkCode; this->vkCode = vkCode;
this->hotkeyID = hotkeyID;
} }
bool operator<(const Shortcut& key) const bool operator<(const Shortcut& key) const

View File

@@ -3,6 +3,7 @@
#include "auto_start_helper.h" #include "auto_start_helper.h"
#include "tray_icon.h" #include "tray_icon.h"
#include "Generated files/resource.h" #include "Generated files/resource.h"
#include "hotkey_conflict_detector.h"
#include <common/SettingsAPI/settings_helpers.h> #include <common/SettingsAPI/settings_helpers.h>
#include "powertoy_module.h" #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); Logger::info(L"apply_general_settings: Enabling powertoy {}", name);
powertoy->enable(); powertoy->enable();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.EnableHotkeyByModule(name);
} }
else else
{ {
Logger::info(L"apply_general_settings: Disabling powertoy {}", name); Logger::info(L"apply_general_settings: Disabling powertoy {}", name);
powertoy->disable(); 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. // Sync the hotkey state with the module state, so it can be removed for disabled modules.
powertoy.UpdateHotkeyEx(); powertoy.UpdateHotkeyEx();
@@ -315,6 +320,8 @@ void start_enabled_powertoys()
{ {
Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name); Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name);
powertoy->enable(); powertoy->enable();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.EnableHotkeyByModule(name);
powertoy.UpdateHotkeyEx(); powertoy.UpdateHotkeyEx();
} }
} }

View File

@@ -0,0 +1,471 @@
#include "pch.h"
#include "hotkey_conflict_detector.h"
#include <common/SettingsAPI/settings_helpers.h>
#include <windows.h>
#include <unordered_map>
#include <cwchar>
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<unsigned char>(shortcut.vkCode);
return hotkey;
}
HotkeyConflictManager* HotkeyConflictManager::instance = nullptr;
std::mutex HotkeyConflictManager::instanceMutex;
HotkeyConflictManager& HotkeyConflictManager::GetInstance()
{
std::lock_guard<std::mutex> 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<HotkeyConflictInfo> HotkeyConflictManager::GetAllConflicts(Hotkey const& _hotkey)
{
std::vector<HotkeyConflictInfo> 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<HotkeyConflictInfo> HotkeyConflictManager::RemoveHotkeyByModule(const std::wstring& moduleName)
{
std::vector<HotkeyConflictInfo> removedHotkeys;
if (disabledHotkeys.find(moduleName) != disabledHotkeys.end())
{
disabledHotkeys.erase(moduleName);
}
std::lock_guard<std::mutex> 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<std::mutex> 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<int>(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;
}
}

View File

@@ -0,0 +1,100 @@
#pragma once
#include "pch.h"
#include <unordered_map>
#include <unordered_set>
#include <string>
#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<HotkeyConflictInfo> HotkeyConflictManager::GetAllConflicts(Hotkey const& hotkey);
bool AddHotkey(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID, bool isEnabled);
std::vector<HotkeyConflictInfo> 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<uint16_t, HotkeyConflictInfo> hotkeyMap;
// Hotkey in sysConflictHotkeyMap means the hotkey has conflict with system defined hotkeys
std::unordered_map<uint16_t, std::unordered_set<HotkeyConflictInfo>> sysConflictHotkeyMap;
// Hotkey in inAppConflictHotkeyMap means the hotkey has conflict with other modules
std::unordered_map<uint16_t, std::unordered_set<HotkeyConflictInfo>> inAppConflictHotkeyMap;
std::unordered_map<std::wstring, std::vector<HotkeyConflictInfo>> disabledHotkeys;
uint16_t GetHotkeyHandle(const Hotkey&);
bool HasConflictWithSystemHotkey(const Hotkey&);
HotkeyConflictManager() = default;
};
};
namespace std
{
template<>
struct hash<HotkeyConflictDetector::HotkeyConflictInfo>
{
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<size_t>(info.hotkey.key) << 4);
size_t moduleHash = std::hash<std::wstring>{}(info.moduleName);
size_t idHash = std::hash<int>{}(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
}
};
}

View File

@@ -40,13 +40,14 @@ json::JsonObject PowertoyModule::json_config() const
} }
PowertoyModule::PowertoyModule(PowertoyModuleIface* pt_module, HMODULE handle) : 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) if (!pt_module)
{ {
throw std::runtime_error("Module not initialized"); throw std::runtime_error("Module not initialized");
} }
remove_hotkey_records();
update_hotkeys(); update_hotkeys();
UpdateHotkeyEx(); UpdateHotkeyEx();
} }
@@ -63,19 +64,27 @@ void PowertoyModule::update_hotkeys()
for (size_t i = 0; i < hotkeyCount; i++) for (size_t i = 0; i < hotkeyCount; i++)
{ {
CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] { if (hotkeys[i].isShown)
Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key()); {
return modulePtr->on_hotkey(i); hkmng.AddHotkey(hotkeys[i], pt_module->get_key(), static_cast<int>(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() void PowertoyModule::UpdateHotkeyEx()
{ {
CentralizedHotkeys::UnregisterHotkeysForModule(pt_module->get_key()); CentralizedHotkeys::UnregisterHotkeysForModule(pt_module->get_key());
auto container = pt_module->GetHotkeyEx(); auto container = pt_module->GetHotkeyEx();
if (container.has_value() && pt_module->is_enabled()) if (container.has_value() && pt_module->is_enabled())
{ {
hkmng.RemoveHotkeyByModule(pt_module->get_key());
auto hotkey = container.value(); auto hotkey = container.value();
auto modulePtr = pt_module.get(); auto modulePtr = pt_module.get();
auto action = [modulePtr](WORD /*modifiersMask*/, WORD /*vkCode*/) { auto action = [modulePtr](WORD /*modifiersMask*/, WORD /*vkCode*/) {
@@ -83,6 +92,9 @@ void PowertoyModule::UpdateHotkeyEx()
modulePtr->OnHotkeyEx(); 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 }); CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode }, { pt_module->get_key(), action });
} }

View File

@@ -5,6 +5,7 @@
#include <mutex> #include <mutex>
#include <vector> #include <vector>
#include <functional> #include <functional>
#include "hotkey_conflict_detector.h"
#include <common/utils/json.h> #include <common/utils/json.h>
@@ -44,9 +45,17 @@ public:
void UpdateHotkeyEx(); void UpdateHotkeyEx();
inline void remove_hotkey_records()
{
hkmng.RemoveHotkeyByModule(pt_module->get_key());
}
private: private:
HotkeyConflictDetector::HotkeyConflictManager& hkmng;
std::unique_ptr<HMODULE, PowertoyModuleDLLDeleter> handle; std::unique_ptr<HMODULE, PowertoyModuleDLLDeleter> handle;
std::unique_ptr<PowertoyModuleIface, PowertoyModuleDeleter> pt_module; std::unique_ptr<PowertoyModuleIface, PowertoyModuleDeleter> pt_module;
}; };
PowertoyModule load_powertoy(const std::wstring_view filename); PowertoyModule load_powertoy(const std::wstring_view filename);

View File

@@ -51,6 +51,7 @@
<ClCompile Include="bug_report.cpp" /> <ClCompile Include="bug_report.cpp" />
<ClCompile Include="centralized_hotkeys.cpp" /> <ClCompile Include="centralized_hotkeys.cpp" />
<ClCompile Include="general_settings.cpp" /> <ClCompile Include="general_settings.cpp" />
<ClCompile Include="hotkey_conflict_detector.cpp" />
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile> </ClCompile>
@@ -71,6 +72,7 @@
<ClInclude Include="bug_report.h" /> <ClInclude Include="bug_report.h" />
<ClInclude Include="centralized_hotkeys.h" /> <ClInclude Include="centralized_hotkeys.h" />
<ClInclude Include="general_settings.h" /> <ClInclude Include="general_settings.h" />
<ClInclude Include="hotkey_conflict_detector.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="centralized_kb_hook.h" /> <ClInclude Include="centralized_kb_hook.h" />
<ClInclude Include="settings_telemetry.h" /> <ClInclude Include="settings_telemetry.h" />

View File

@@ -45,6 +45,9 @@
<ClCompile Include="bug_report.cpp"> <ClCompile Include="bug_report.cpp">
<Filter>Utils</Filter> <Filter>Utils</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="hotkey_conflict_detector.cpp">
<Filter>Utils</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
@@ -93,6 +96,9 @@
<ClInclude Include="bug_report.h"> <ClInclude Include="bug_report.h">
<Filter>Utils</Filter> <Filter>Utils</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="hotkey_conflict_detector.h">
<Filter>Utils</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Filter Include="Utils"> <Filter Include="Utils">

View File

@@ -13,6 +13,7 @@
#include "UpdateUtils.h" #include "UpdateUtils.h"
#include "centralized_kb_hook.h" #include "centralized_kb_hook.h"
#include "Generated files/resource.h" #include "Generated files/resource.h"
#include "hotkey_conflict_detector.h"
#include <common/utils/json.h> #include <common/utils/json.h>
#include <common/SettingsAPI/settings_helpers.cpp> #include <common/SettingsAPI/settings_helpers.cpp>
@@ -153,6 +154,8 @@ void send_json_config_to_module(const std::wstring& module_key, const std::wstri
if (moduleIt != modules().end()) if (moduleIt != modules().end())
{ {
moduleIt->second->set_config(settings.c_str()); moduleIt->second->set_config(settings.c_str());
moduleIt->second.remove_hotkey_records();
moduleIt->second.update_hotkeys(); moduleIt->second.update_hotkeys();
moduleIt->second.UpdateHotkeyEx(); 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; const std::wstring save_file_location = PTSettingsHelper::get_root_save_folder_location() + language_filename;
json::to_file(save_file_location, j); 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<unsigned char>(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; return;
} }

View File

@@ -12,7 +12,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction
{ {
private HotkeySettings _shortcut = new(); private HotkeySettings _shortcut = new();
private bool _isShown = true; private bool _isShown;
private bool _hasConflict;
private string _tooltip;
[JsonPropertyName("shortcut")] [JsonPropertyName("shortcut")]
public HotkeySettings Shortcut public HotkeySettings Shortcut
@@ -38,6 +40,20 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
set => Set(ref _isShown, value); 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] [JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => []; public IEnumerable<IAdvancedPasteAction> SubActions => [];
} }

View File

@@ -28,16 +28,23 @@ public sealed class AdvancedPasteAdditionalActions
public IEnumerable<IAdvancedPasteAction> GetAllActions() public IEnumerable<IAdvancedPasteAction> GetAllActions()
{ {
Queue<IAdvancedPasteAction> queue = new([ImageToText, PasteAsFile, Transcode]); return GetAllActionsRecursive([ImageToText, PasteAsFile, Transcode]);
}
while (queue.Count != 0) /// <summary>
/// Changed to depth-first traversal to ensure ordered output
/// </summary>
/// <param name="actions">The collection of actions to traverse</param>
/// <returns>All actions returned in depth-first order</returns>
private static IEnumerable<IAdvancedPasteAction> GetAllActionsRecursive(IEnumerable<IAdvancedPasteAction> actions)
{
foreach (var action in actions)
{ {
var action = queue.Dequeue();
yield return action; yield return action;
foreach (var subAction in action.SubActions) foreach (var subAction in GetAllActionsRecursive(action.SubActions))
{ {
queue.Enqueue(subAction); yield return subAction;
} }
} }
} }

View File

@@ -5,8 +5,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Library; namespace Microsoft.PowerToys.Settings.UI.Library;
@@ -20,6 +20,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private bool _canMoveUp; private bool _canMoveUp;
private bool _canMoveDown; private bool _canMoveDown;
private bool _isValid; private bool _isValid;
private bool _hasConflict;
private string _tooltip;
[JsonPropertyName("id")] [JsonPropertyName("id")]
public int 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 // 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. // with null; the ShortcutControl depends on this.
_shortcut = value ?? new(); _shortcut = value ?? new();
OnPropertyChanged(); OnPropertyChanged();
} }
} }
@@ -99,6 +100,20 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private set => Set(ref _isValid, value); 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] [JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => []; public IEnumerable<IAdvancedPasteAction> SubActions => [];
@@ -118,6 +133,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
IsShown = other.IsShown; IsShown = other.IsShown;
CanMoveUp = other.CanMoveUp; CanMoveUp = other.CanMoveUp;
CanMoveDown = other.CanMoveDown; CanMoveDown = other.CanMoveDown;
HasConflict = other.HasConflict;
Tooltip = other.Tooltip;
} }
private HotkeySettings GetShortcutClone() private HotkeySettings GetShortcutClone()

View File

@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "AdvancedPaste"; public const string ModuleName = "AdvancedPaste";
@@ -39,6 +41,64 @@ namespace Microsoft.PowerToys.Settings.UI.Library
settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName); settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName);
} }
public ModuleType GetModuleType() => ModuleType.AdvancedPaste;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
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() public string GetModuleName()
=> Name; => Name;

View File

@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library 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 ModuleName = "AlwaysOnTop";
public const string ModuleVersion = "0.0.1"; public const string ModuleVersion = "0.0.1";
@@ -28,6 +30,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.AlwaysOnTop;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.Hotkey.Value,
value => Properties.Hotkey.Value = value ?? AlwaysOnTopProperties.DefaultHotkeyValue,
"AlwaysOnTop_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
{ {
return false; return false;

View File

@@ -7,14 +7,14 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations; using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class ColorPickerSettings : BasePTModuleSettings, ISettingsConfig public class ColorPickerSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "ColorPicker"; public const string ModuleName = "ColorPicker";
@@ -64,6 +64,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return false; return false;
} }
public ModuleType GetModuleType() => ModuleType.ColorPicker;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"Activation_Shortcut"),
};
return hotkeyAccessors.ToArray();
}
public static object UpgradeSettings(object oldSettingsObject) public static object UpgradeSettings(object oldSettingsObject)
{ {
ColorPickerSettingsVersion1 oldSettings = (ColorPickerSettingsVersion1)oldSettingsObject; ColorPickerSettingsVersion1 oldSettings = (ColorPickerSettingsVersion1)oldSettingsObject;

View File

@@ -5,7 +5,6 @@
using System; using System;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library

View File

@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library 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 ModuleName = "CropAndLock";
public const string ModuleVersion = "0.0.1"; public const string ModuleVersion = "0.0.1";
@@ -28,6 +30,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.CropAndLock;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
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() public bool UpgradeSettingsConfiguration()
{ {
return false; return false;

View File

@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "FindMyMouse"; public const string ModuleName = "FindMyMouse";
@@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.FindMyMouse;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
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. // This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
{ {

View File

@@ -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<HotkeySettings> Getter { get; }
public Action<HotkeySettings> Setter { get; }
public HotkeyAccessor(Func<HotkeySettings> getter, Action<HotkeySettings> 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; }
}
}

View File

@@ -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<HotkeyConflictGroupData> InAppConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public List<HotkeyConflictGroupData> SystemConflicts { get; set; } = new List<HotkeyConflictGroupData>();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<ModuleHotkeyData> Modules { get; set; }
}
}

View File

@@ -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<string> AllConflictingModules { get; set; } = new List<string>();
}
}

View File

@@ -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<object> GetKeysList()
{
List<object> shortcutList = new List<object>();
if (Win)
{
shortcutList.Add(92); // The Windows key or button.
}
if (Ctrl)
{
shortcutList.Add("Ctrl");
}
if (Alt)
{
shortcutList.Add("Alt");
}
if (Shift)
{
shortcutList.Add(16); // The Shift key or button.
}
if (Key > 0)
{
switch (Key)
{
// https://learn.microsoft.com/uwp/api/windows.system.virtualkey?view=winrt-20348
case 38: // The Up Arrow key or button.
case 40: // The Down Arrow key or button.
case 37: // The Left Arrow key or button.
case 39: // The Right Arrow key or button.
shortcutList.Add(Key);
break;
default:
var localKey = Helper.GetKeyName((uint)Key);
shortcutList.Add(localKey);
break;
}
}
return shortcutList;
}
}
}

View File

@@ -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 ModuleConflictsData
{
public List<HotkeyConflictGroupData> InAppConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public List<HotkeyConflictGroupData> SystemConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public bool HasConflicts => InAppConflicts.Count > 0 || SystemConflicts.Count > 0;
}
}

View File

@@ -0,0 +1,84 @@
// 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.ComponentModel;
using System.Runtime.CompilerServices;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Windows.Web.AtomPub;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class ModuleHotkeyData : INotifyPropertyChanged
{
private string _moduleName;
private int _hotkeyID;
private HotkeySettings _hotkeySettings;
private bool _isSystemConflict;
public event PropertyChangedEventHandler PropertyChanged;
public string IconPath { get; set; }
public string DisplayName { get; set; }
public string Header { get; set; }
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string ModuleName
{
get => _moduleName;
set
{
if (_moduleName != value)
{
_moduleName = value;
}
}
}
public int HotkeyID
{
get => _hotkeyID;
set
{
if (_hotkeyID != value)
{
_hotkeyID = value;
}
}
}
public HotkeySettings HotkeySettings
{
get => _hotkeySettings;
set
{
if (_hotkeySettings != value)
{
_hotkeySettings = value;
OnPropertyChanged();
}
}
}
public bool IsSystemConflict
{
get => _isSystemConflict;
set
{
if (_isSystemConflict != value)
{
_isSystemConflict = value;
}
}
}
public ModuleType ModuleType { get; set; }
}
}

View File

@@ -4,17 +4,29 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public record HotkeySettings : ICmdLineRepresentable public record HotkeySettings : ICmdLineRepresentable, INotifyPropertyChanged
{ {
private const int VKTAB = 0x09; private const int VKTAB = 0x09;
private bool _hasConflict;
private string _conflictDescription;
private bool _isSystemConflict;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public HotkeySettings() public HotkeySettings()
{ {
@@ -23,6 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = false; Alt = false;
Shift = false; Shift = false;
Code = 0; Code = 0;
HasConflict = false;
} }
/// <summary> /// <summary>
@@ -40,6 +54,51 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = alt; Alt = alt;
Shift = shift; Shift = shift;
Code = code; Code = code;
HasConflict = false;
}
public bool HasConflict
{
get => _hasConflict;
set
{
if (_hasConflict != value)
{
_hasConflict = value;
OnPropertyChanged();
}
}
}
public string ConflictDescription
{
get => _conflictDescription ?? string.Empty;
set
{
if (_conflictDescription != value)
{
_conflictDescription = value;
OnPropertyChanged();
}
}
}
public bool IsSystemConflict
{
get => _isSystemConflict;
set
{
if (_isSystemConflict != value)
{
_isSystemConflict = value;
OnPropertyChanged();
}
}
}
public virtual void UpdateConflictStatus()
{
Logger.LogInfo($"{this.ToString()}");
} }
[JsonPropertyName("win")] [JsonPropertyName("win")]

View File

@@ -0,0 +1,17 @@
// 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.Collections.Generic;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces
{
public interface IHotkeyConfig
{
HotkeyAccessor[] GetAllHotkeyAccessors();
ModuleType GetModuleType();
}
}

View File

@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "Measure Tool"; public const string ModuleName = "Measure Tool";
@@ -25,6 +27,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string GetModuleName() public string GetModuleName()
=> Name; => Name;
public ModuleType GetModuleType() => ModuleType.MeasureTool;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"MeasureTool_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted. // This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
=> false; => false;

View File

@@ -2,15 +2,17 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "MouseHighlighter"; public const string ModuleName = "MouseHighlighter";
@@ -29,6 +31,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.MouseHighlighter;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"MouseUtils_MouseHighlighter_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted. // This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
{ {

View File

@@ -3,16 +3,18 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using MouseJump.Common.Helpers; using MouseJump.Common.Helpers;
using MouseJump.Common.Models.Settings; using MouseJump.Common.Models.Settings;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class MouseJumpSettings : BasePTModuleSettings, ISettingsConfig public class MouseJumpSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "MouseJump"; public const string ModuleName = "MouseJump";
@@ -46,6 +48,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.MouseJump;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"MouseUtils_MouseJump_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted. // This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
{ {

View File

@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class MousePointerCrosshairsSettings : BasePTModuleSettings, ISettingsConfig public class MousePointerCrosshairsSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "MousePointerCrosshairs"; public const string ModuleName = "MousePointerCrosshairs";
@@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.MousePointerCrosshairs;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"MouseUtils_MousePointerCrosshairs_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted. // This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
{ {

View File

@@ -3,15 +3,17 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class MouseWithoutBordersSettings : BasePTModuleSettings, ISettingsConfig public class MouseWithoutBordersSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "MouseWithoutBorders"; public const string ModuleName = "MouseWithoutBorders";
@@ -37,6 +39,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.MouseWithoutBorders;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ToggleEasyMouseShortcut,
value => Properties.ToggleEasyMouseShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyToggleEasyMouse,
"MouseWithoutBorders_ToggleEasyMouseShortcut"),
new HotkeyAccessor(
() => Properties.LockMachineShortcut,
value => Properties.LockMachineShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyLockMachine,
"MouseWithoutBorders_LockMachinesShortcut"),
new HotkeyAccessor(
() => Properties.Switch2AllPCShortcut,
value => Properties.Switch2AllPCShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeySwitch2AllPC,
"MouseWithoutBorders_Switch2AllPcShortcut"),
new HotkeyAccessor(
() => Properties.ReconnectShortcut,
value => Properties.ReconnectShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyReconnect,
"MouseWithoutBorders_ReconnectShortcut"),
};
return hotkeyAccessors.ToArray();
}
public HotkeySettings ConvertMouseWithoutBordersHotKeyToPowerToys(int value) public HotkeySettings ConvertMouseWithoutBordersHotKeyToPowerToys(int value)
{ {
// VK_A <= value <= VK_Z // VK_A <= value <= VK_Z

View File

@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class PeekSettings : BasePTModuleSettings, ISettingsConfig public class PeekSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "Peek"; public const string ModuleName = "Peek";
public const string ModuleVersion = "0.0.1"; public const string ModuleVersion = "0.0.1";
@@ -35,6 +37,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.Peek;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"Activation_Shortcut"),
};
return hotkeyAccessors.ToArray();
}
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
{ {
return false; return false;

View File

@@ -6,12 +6,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class PowerLauncherSettings : BasePTModuleSettings, ISettingsConfig public class PowerLauncherSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "PowerToys Run"; public const string ModuleName = "PowerToys Run";
@@ -49,6 +50,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.PowerLauncher;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.OpenPowerLauncher,
value => Properties.OpenPowerLauncher = value ?? Properties.DefaultOpenPowerLauncher,
"PowerLauncher_OpenPowerLauncher"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted. // This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
{ {

View File

@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class PowerOcrSettings : BasePTModuleSettings, ISettingsConfig public class PowerOcrSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "TextExtractor"; public const string ModuleName = "TextExtractor";
@@ -42,6 +44,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string GetModuleName() public string GetModuleName()
=> Name; => Name;
public ModuleType GetModuleType() => ModuleType.PowerOCR;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"Activation_Shortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted. // This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
=> false; => false;

View File

@@ -0,0 +1,197 @@
// 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.Reflection;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Services
{
/// <summary>
/// Factory service for getting PowerToys module Settings that implement IHotkeyConfig
/// </summary>
public class SettingsFactory
{
private readonly ISettingsUtils _settingsUtils;
private readonly Dictionary<string, Type> _settingsTypes;
public SettingsFactory(ISettingsUtils settingsUtils)
{
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
_settingsTypes = DiscoverSettingsTypes();
}
/// <summary>
/// Dynamically discovers all Settings types that implement IHotkeyConfig
/// </summary>
private Dictionary<string, Type> DiscoverSettingsTypes()
{
var settingsTypes = new Dictionary<string, Type>();
// Get the Settings.UI.Library assembly
var assembly = Assembly.GetAssembly(typeof(IHotkeyConfig));
if (assembly == null)
{
return settingsTypes;
}
try
{
// Find all types that implement IHotkeyConfig and ISettingsConfig
var hotkeyConfigTypes = assembly.GetTypes()
.Where(type =>
type.IsClass &&
!type.IsAbstract &&
typeof(IHotkeyConfig).IsAssignableFrom(type) &&
typeof(ISettingsConfig).IsAssignableFrom(type))
.ToList();
foreach (var type in hotkeyConfigTypes)
{
// Try to get the ModuleName using SettingsRepository
try
{
var repositoryType = typeof(SettingsRepository<>).MakeGenericType(type);
var getInstanceMethod = repositoryType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static);
var repository = getInstanceMethod?.Invoke(null, new object[] { _settingsUtils });
if (repository != null)
{
var settingsConfigProperty = repository.GetType().GetProperty("SettingsConfig");
var settingsInstance = settingsConfigProperty?.GetValue(repository) as ISettingsConfig;
if (settingsInstance != null)
{
var moduleName = settingsInstance.GetModuleName();
if (!string.IsNullOrEmpty(moduleName))
{
settingsTypes[moduleName] = type;
System.Diagnostics.Debug.WriteLine($"Discovered settings type: {type.Name} for module: {moduleName}");
}
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error getting module name for {type.Name}: {ex.Message}");
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error scanning assembly {assembly.FullName}: {ex.Message}");
}
return settingsTypes;
}
public IHotkeyConfig GetFreshSettings(string moduleKey)
{
if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType))
{
return null;
}
try
{
// Create a generic method call to _settingsUtils.GetSettingsOrDefault<T>(moduleKey)
var getSettingsMethod = typeof(ISettingsUtils).GetMethod("GetSettingsOrDefault", new[] { typeof(string), typeof(string) });
var genericMethod = getSettingsMethod?.MakeGenericMethod(settingsType);
// Call GetSettingsOrDefault<T>(moduleKey) to get fresh settings from file
var freshSettings = genericMethod?.Invoke(_settingsUtils, new object[] { moduleKey, "settings.json" });
return freshSettings as IHotkeyConfig;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error getting fresh settings for {moduleKey}: {ex.Message}");
return null;
}
}
/// <summary>
/// Gets a settings instance for the specified module using SettingsRepository
/// </summary>
/// <param name="moduleKey">The module key/name</param>
/// <returns>The settings instance implementing IHotkeyConfig, or null if not found</returns>
public IHotkeyConfig GetSettings(string moduleKey)
{
if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType))
{
return null;
}
try
{
var repositoryType = typeof(SettingsRepository<>).MakeGenericType(settingsType);
var getInstanceMethod = repositoryType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static);
var repository = getInstanceMethod?.Invoke(null, new object[] { _settingsUtils });
if (repository != null)
{
var settingsConfigProperty = repository.GetType().GetProperty("SettingsConfig");
return settingsConfigProperty?.GetValue(repository) as IHotkeyConfig;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error getting Settings for {moduleKey}: {ex.Message}");
}
return null;
}
/// <summary>
/// Gets all available module names that have settings implementing IHotkeyConfig
/// </summary>
/// <returns>List of module names</returns>
public List<string> GetAvailableModuleNames()
{
return _settingsTypes.Keys.ToList();
}
/// <summary>
/// Gets all available settings that implement IHotkeyConfig
/// </summary>
/// <returns>Dictionary of module name to settings instance</returns>
public Dictionary<string, IHotkeyConfig> GetAllHotkeySettings()
{
var result = new Dictionary<string, IHotkeyConfig>();
foreach (var moduleKey in _settingsTypes.Keys)
{
try
{
var settings = GetSettings(moduleKey);
if (settings != null)
{
result[moduleKey] = settings;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error getting settings for {moduleKey}: {ex.Message}");
}
}
return result;
}
/// <summary>
/// Gets a specific settings repository instance
/// </summary>
/// <typeparam name="T">The settings type</typeparam>
/// <returns>The settings repository instance</returns>
public ISettingsRepository<T> GetRepository<T>()
where T : class, ISettingsConfig, new()
{
return SettingsRepository<T>.GetInstance(_settingsUtils);
}
}
}

View File

@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class ShortcutGuideSettings : BasePTModuleSettings, ISettingsConfig public class ShortcutGuideSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "Shortcut Guide"; public const string ModuleName = "Shortcut Guide";
@@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name; return Name;
} }
public ModuleType GetModuleType() => ModuleType.ShortcutGuide;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.OpenShortcutGuide,
value => Properties.OpenShortcutGuide = value ?? Properties.DefaultOpenShortcutGuide,
"Activation_Shortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted. // This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration() public bool UpgradeSettingsConfiguration()
{ {

View File

@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public class WorkspacesSettings : BasePTModuleSettings, ISettingsConfig public class WorkspacesSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{ {
public const string ModuleName = "Workspaces"; public const string ModuleName = "Workspaces";
public const string ModuleVersion = "0.0.1"; public const string ModuleVersion = "0.0.1";
@@ -39,6 +41,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return false; return false;
} }
public ModuleType GetModuleType() => ModuleType.Workspaces;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.Hotkey.Value,
value => Properties.Hotkey.Value = value ?? WorkspacesProperties.DefaultHotkeyValue,
"Workspaces_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
public virtual void Save(ISettingsUtils settingsUtils) public virtual void Save(ISettingsUtils settingsUtils)
{ {
// Save settings to file // Save settings to file

View File

@@ -3,8 +3,8 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility; using Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility;
using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -13,7 +13,7 @@ using Moq;
namespace ViewModelTests namespace ViewModelTests
{ {
[TestClass] [TestClass]
public class PowerLauncherViewModelTest public class PowerLauncherViewModelTest : IDisposable
{ {
private sealed class SendCallbackMock private sealed class SendCallbackMock
{ {
@@ -26,20 +26,48 @@ namespace ViewModelTests
{ {
TimesSent++; TimesSent++;
} }
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "We actually don't validate setting, just calculate it was sent")]
public int OnSendIPC(string _)
{
TimesSent++;
return 0;
}
} }
private PowerLauncherViewModel viewModel; private PowerLauncherViewModel viewModel;
private PowerLauncherSettings mockSettings; private PowerLauncherSettings mockSettings;
private SendCallbackMock sendCallbackMock; private SendCallbackMock sendCallbackMock;
private BackCompatTestProperties.MockSettingsRepository<GeneralSettings> mockGeneralSettingsRepository;
[TestInitialize] [TestInitialize]
public void Initialize() public void Initialize()
{ {
mockSettings = new PowerLauncherSettings(); mockSettings = new PowerLauncherSettings();
sendCallbackMock = new SendCallbackMock(); sendCallbackMock = new SendCallbackMock();
var settingPathMock = new Mock<ISettingsPath>();
var mockGeneralIOProvider = BackCompatTestProperties.GetGeneralSettingsIOProvider("v0.22.0");
var mockGeneralSettingsUtils = new SettingsUtils(mockGeneralIOProvider.Object, settingPathMock.Object);
mockGeneralSettingsRepository = new BackCompatTestProperties.MockSettingsRepository<GeneralSettings>(mockGeneralSettingsUtils);
viewModel = new PowerLauncherViewModel( viewModel = new PowerLauncherViewModel(
mockSettings, mockSettings,
new PowerLauncherViewModel.SendCallback(sendCallbackMock.OnSend)); mockGeneralSettingsRepository,
sendCallbackMock.OnSendIPC,
() => false);
}
[TestCleanup]
public void Cleanup()
{
viewModel?.Dispose();
}
public void Dispose()
{
viewModel?.Dispose();
GC.SuppressFinalize(this);
} }
/// <summary> /// <summary>
@@ -67,7 +95,7 @@ namespace ViewModelTests
// Initialise View Model with test Config files // Initialise View Model with test Config files
Func<string, int> sendMockIPCConfigMSG = msg => { return 0; }; Func<string, int> sendMockIPCConfigMSG = msg => { return 0; };
PowerLauncherViewModel viewModel = new PowerLauncherViewModel(originalSettings, generalSettingsRepository, sendMockIPCConfigMSG, () => true); using PowerLauncherViewModel viewModel = new PowerLauncherViewModel(originalSettings, generalSettingsRepository, sendMockIPCConfigMSG, () => true);
// Verify that the old settings persisted // Verify that the old settings persisted
Assert.AreEqual(originalGeneralSettings.Enabled.PowerLauncher, viewModel.EnablePowerLauncher); Assert.AreEqual(originalGeneralSettings.Enabled.PowerLauncher, viewModel.EnablePowerLauncher);

View File

@@ -0,0 +1,27 @@
// 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 Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
public partial class BoolToConflictTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool isSystemConflict)
{
return isSystemConflict ? "System Conflict" : "In-App Conflict";
}
return "Unknown Conflict";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,73 @@
// 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.Text.Json.Nodes;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.Helpers
{
public class HotkeyConflictHelper
{
public delegate void HotkeyConflictCheckCallback(bool hasConflict, HotkeyConflictResponse conflicts);
private static readonly Dictionary<string, HotkeyConflictCheckCallback> PendingHotkeyConflictChecks = new Dictionary<string, HotkeyConflictCheckCallback>();
private static readonly object LockObject = new object();
public static void CheckHotkeyConflict(HotkeySettings hotkeySettings, Func<string, int> ipcMSGCallBackFunc, HotkeyConflictCheckCallback callback)
{
if (hotkeySettings == null || ipcMSGCallBackFunc == null)
{
return;
}
string requestId = GenerateRequestId();
lock (LockObject)
{
PendingHotkeyConflictChecks[requestId] = callback;
}
var hotkeyObj = new JsonObject
{
["request_id"] = requestId,
["win"] = hotkeySettings.Win,
["ctrl"] = hotkeySettings.Ctrl,
["shift"] = hotkeySettings.Shift,
["alt"] = hotkeySettings.Alt,
["key"] = hotkeySettings.Code,
};
var requestObject = new JsonObject
{
["check_hotkey_conflict"] = hotkeyObj,
};
ipcMSGCallBackFunc(requestObject.ToString());
}
public static void HandleHotkeyConflictResponse(HotkeyConflictResponse response)
{
if (response.AllConflicts.Count == 0)
{
return;
}
HotkeyConflictCheckCallback callback = null;
lock (LockObject)
{
if (PendingHotkeyConflictChecks.TryGetValue(response.RequestId, out callback))
{
PendingHotkeyConflictChecks.Remove(response.RequestId);
}
}
callback?.Invoke(response.HasConflict, response);
}
private static string GenerateRequestId() => Guid.NewGuid().ToString();
}
}

View File

@@ -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;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Helpers
{
public class HotkeyConflictResponse
{
public string RequestId { get; set; }
public bool HasConflict { get; set; }
public List<ModuleHotkeyData> AllConflicts { get; set; } = new List<ModuleHotkeyData>();
}
}

View File

@@ -13,23 +13,29 @@ using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.SerializationContext; namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(WINDOWPLACEMENT))] [JsonSerializable(typeof(ActionMessage))]
[JsonSerializable(typeof(AdvancedPasteSettings))] [JsonSerializable(typeof(AdvancedPasteSettings))]
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
[JsonSerializable(typeof(AlwaysOnTopSettings))] [JsonSerializable(typeof(AlwaysOnTopSettings))]
[JsonSerializable(typeof(ColorPickerSettings))] [JsonSerializable(typeof(ColorPickerSettings))]
[JsonSerializable(typeof(CropAndLockSettings))] [JsonSerializable(typeof(CropAndLockSettings))]
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
[JsonSerializable(typeof(FileLocksmithSettings))] [JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(MeasureToolSettings))] [JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
[JsonSerializable(typeof(MouseJumpSettings))]
[JsonSerializable(typeof(MousePointerCrosshairsSettings))]
[JsonSerializable(typeof(MouseWithoutBordersSettings))] [JsonSerializable(typeof(MouseWithoutBordersSettings))]
[JsonSerializable(typeof(NewPlusSettings))] [JsonSerializable(typeof(NewPlusSettings))]
[JsonSerializable(typeof(PeekSettings))] [JsonSerializable(typeof(PeekSettings))]
[JsonSerializable(typeof(PowerLauncherSettings))] [JsonSerializable(typeof(PowerLauncherSettings))]
[JsonSerializable(typeof(PowerOcrSettings))] [JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(RegistryPreviewSettings))] [JsonSerializable(typeof(RegistryPreviewSettings))]
[JsonSerializable(typeof(ShortcutGuideSettings))]
[JsonSerializable(typeof(WINDOWPLACEMENT))]
[JsonSerializable(typeof(WorkspacesSettings))] [JsonSerializable(typeof(WorkspacesSettings))]
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(ActionMessage))]
public sealed partial class SourceGenerationContextContext : JsonSerializerContext public sealed partial class SourceGenerationContextContext : JsonSerializerContext
{ {
} }

View File

@@ -0,0 +1,121 @@
// 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 Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Services
{
public class GlobalHotkeyConflictManager
{
private readonly Func<string, int> _sendIPCMessage;
private static GlobalHotkeyConflictManager _instance;
private AllHotkeyConflictsData _currentConflicts = new AllHotkeyConflictsData();
public static GlobalHotkeyConflictManager Instance => _instance;
public static void Initialize(Func<string, int> sendIPCMessage)
{
_instance = new GlobalHotkeyConflictManager(sendIPCMessage);
}
private GlobalHotkeyConflictManager(Func<string, int> sendIPCMessage)
{
_sendIPCMessage = sendIPCMessage;
IPCResponseService.AllHotkeyConflictsReceived += OnAllHotkeyConflictsReceived;
}
public event EventHandler<AllHotkeyConflictsEventArgs> ConflictsUpdated;
public void RequestAllConflicts()
{
var requestMessage = "{\"get_all_hotkey_conflicts\":{}}";
_sendIPCMessage?.Invoke(requestMessage);
}
private void OnAllHotkeyConflictsReceived(object sender, AllHotkeyConflictsEventArgs e)
{
_currentConflicts = e.Conflicts;
ConflictsUpdated?.Invoke(this, e);
}
public bool HasConflictForHotkey(HotkeySettings hotkey, string moduleName, int hotkeyID)
{
if (hotkey == null)
{
return false;
}
var allConflictGroups = _currentConflicts.InAppConflicts.Concat(_currentConflicts.SystemConflicts);
foreach (var group in allConflictGroups)
{
if (IsHotkeyMatch(hotkey, group.Hotkey))
{
if (!string.IsNullOrEmpty(moduleName) && hotkeyID >= 0)
{
var selfModule = group.Modules.FirstOrDefault(m =>
m.ModuleName.Equals(moduleName, StringComparison.OrdinalIgnoreCase) &&
m.HotkeyID == hotkeyID);
if (selfModule != null && group.Modules.Count == 1)
{
return false;
}
}
return true;
}
}
return false;
}
public HotkeyConflictInfo GetConflictInfo(HotkeySettings hotkey)
{
if (hotkey == null)
{
return null;
}
var allConflictGroups = _currentConflicts.InAppConflicts.Concat(_currentConflicts.SystemConflicts);
foreach (var group in allConflictGroups)
{
if (IsHotkeyMatch(hotkey, group.Hotkey))
{
var conflictModules = group.Modules.Where(m => m != null).ToList();
if (conflictModules.Count != 0)
{
var firstModule = conflictModules.First();
return new HotkeyConflictInfo
{
IsSystemConflict = group.IsSystemConflict,
ConflictingModuleName = firstModule.ModuleName,
ConflictingHotkeyID = firstModule.HotkeyID,
AllConflictingModules = conflictModules.Select(m => $"{m.ModuleName}:{m.HotkeyID}").ToList(),
};
}
}
}
return null;
}
private bool IsHotkeyMatch(HotkeySettings settings, HotkeyData data)
{
return settings.Win == data.Win &&
settings.Ctrl == data.Ctrl &&
settings.Shift == data.Shift &&
settings.Alt == data.Alt &&
settings.Code == data.Key;
}
}
}

View File

@@ -0,0 +1,199 @@
// 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 Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Views;
using Windows.Data.Json;
namespace Microsoft.PowerToys.Settings.UI.Services
{
public class IPCResponseService
{
private static IPCResponseService _instance;
public static IPCResponseService Instance => _instance ??= new IPCResponseService();
public static event EventHandler<AllHotkeyConflictsEventArgs> AllHotkeyConflictsReceived;
public void RegisterForIPC()
{
ShellPage.ShellHandler?.IPCResponseHandleList.Add(ProcessIPCMessage);
}
public void UnregisterFromIPC()
{
ShellPage.ShellHandler?.IPCResponseHandleList.Remove(ProcessIPCMessage);
}
private void ProcessIPCMessage(JsonObject json)
{
try
{
if (json.TryGetValue("response_type", out IJsonValue responseTypeValue) &&
responseTypeValue.ValueType == JsonValueType.String)
{
string responseType = responseTypeValue.GetString();
if (responseType.Equals("hotkey_conflict_result", StringComparison.Ordinal))
{
ProcessHotkeyConflictResult(json);
}
else if (responseType.Equals("all_hotkey_conflicts", StringComparison.Ordinal))
{
ProcessAllHotkeyConflicts(json);
}
}
}
catch (Exception)
{
}
}
private void ProcessHotkeyConflictResult(JsonObject json)
{
string requestId = string.Empty;
if (json.TryGetValue("request_id", out IJsonValue requestIdValue) &&
requestIdValue.ValueType == JsonValueType.String)
{
requestId = requestIdValue.GetString();
}
bool hasConflict = false;
if (json.TryGetValue("has_conflict", out IJsonValue hasConflictValue) &&
hasConflictValue.ValueType == JsonValueType.Boolean)
{
hasConflict = hasConflictValue.GetBoolean();
}
var allConflicts = new List<ModuleHotkeyData>();
if (hasConflict)
{
// Parse the all_conflicts array
if (json.TryGetValue("all_conflicts", out IJsonValue allConflictsValue) &&
allConflictsValue.ValueType == JsonValueType.Array)
{
var conflictsArray = allConflictsValue.GetArray();
foreach (var conflictItem in conflictsArray)
{
if (conflictItem.ValueType == JsonValueType.Object)
{
var conflictObj = conflictItem.GetObject();
string moduleName = string.Empty;
int hotkeyID = -1;
if (conflictObj.TryGetValue("module", out IJsonValue moduleValue) &&
moduleValue.ValueType == JsonValueType.String)
{
moduleName = moduleValue.GetString();
}
if (conflictObj.TryGetValue("hotkeyID", out IJsonValue hotkeyValue) &&
hotkeyValue.ValueType == JsonValueType.Number)
{
hotkeyID = (int)hotkeyValue.GetNumber();
}
allConflicts.Add(new ModuleHotkeyData
{
ModuleName = moduleName,
HotkeyID = hotkeyID,
});
}
}
}
}
var response = new HotkeyConflictResponse
{
RequestId = requestId,
HasConflict = hasConflict,
AllConflicts = allConflicts,
};
HotkeyConflictHelper.HandleHotkeyConflictResponse(response);
}
private void ProcessAllHotkeyConflicts(JsonObject json)
{
var allConflicts = new AllHotkeyConflictsData();
if (json.TryGetValue("inAppConflicts", out IJsonValue inAppValue) &&
inAppValue.ValueType == JsonValueType.Array)
{
var inAppArray = inAppValue.GetArray();
foreach (var conflictGroup in inAppArray)
{
var conflictObj = conflictGroup.GetObject();
var conflictData = ParseConflictGroup(conflictObj, false);
if (conflictData != null)
{
allConflicts.InAppConflicts.Add(conflictData);
}
}
}
if (json.TryGetValue("sysConflicts", out IJsonValue sysValue) &&
sysValue.ValueType == JsonValueType.Array)
{
var sysArray = sysValue.GetArray();
foreach (var conflictGroup in sysArray)
{
var conflictObj = conflictGroup.GetObject();
var conflictData = ParseConflictGroup(conflictObj, true);
if (conflictData != null)
{
allConflicts.SystemConflicts.Add(conflictData);
}
}
}
AllHotkeyConflictsReceived?.Invoke(this, new AllHotkeyConflictsEventArgs(allConflicts));
}
private HotkeyConflictGroupData ParseConflictGroup(JsonObject conflictObj, bool isSystemConflict)
{
if (!conflictObj.TryGetValue("hotkey", out var hotkeyValue) ||
!conflictObj.TryGetValue("modules", out var modulesValue))
{
return null;
}
var hotkeyObj = hotkeyValue.GetObject();
bool win = hotkeyObj.TryGetValue("win", out var winVal) && winVal.GetBoolean();
bool ctrl = hotkeyObj.TryGetValue("ctrl", out var ctrlVal) && ctrlVal.GetBoolean();
bool shift = hotkeyObj.TryGetValue("shift", out var shiftVal) && shiftVal.GetBoolean();
bool alt = hotkeyObj.TryGetValue("alt", out var altVal) && altVal.GetBoolean();
int key = hotkeyObj.TryGetValue("key", out var keyVal) ? (int)keyVal.GetNumber() : 0;
var conflictGroup = new HotkeyConflictGroupData
{
Hotkey = new HotkeyData { Win = win, Ctrl = ctrl, Shift = shift, Alt = alt, Key = key },
IsSystemConflict = isSystemConflict,
Modules = new List<ModuleHotkeyData>(),
};
var modulesArray = modulesValue.GetArray();
foreach (var module in modulesArray)
{
var moduleObj = module.GetObject();
string moduleName = moduleObj.TryGetValue("moduleName", out var modNameVal) ? modNameVal.GetString() : string.Empty;
int hotkeyID = moduleObj.TryGetValue("hotkeyID", out var hotkeyIDVal) ? (int)hotkeyIDVal.GetNumber() : -1;
conflictGroup.Modules.Add(new ModuleHotkeyData
{
ModuleName = moduleName,
HotkeyID = hotkeyID,
});
}
return conflictGroup;
}
}
}

View File

@@ -232,6 +232,12 @@ namespace Microsoft.PowerToys.Settings.UI
}); });
ipcmanager.Start(); ipcmanager.Start();
GlobalHotkeyConflictManager.Initialize(message =>
{
ipcmanager.Send(message);
return 0;
});
if (!ShowOobe && !ShowScoobe && !ShowFlyout) if (!ShowOobe && !ShowScoobe && !ShowFlyout)
{ {
settingsWindow = new MainWindow(); settingsWindow = new MainWindow();
@@ -320,10 +326,18 @@ namespace Microsoft.PowerToys.Settings.UI
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow)); WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
settingsWindow.Activate(); settingsWindow.Activate();
settingsWindow.NavigateToSection(StartupPage); settingsWindow.NavigateToSection(StartupPage);
// In DEBUG mode, we might not have IPC set up, so provide a dummy implementation
GlobalHotkeyConflictManager.Initialize(message =>
{
// In debug mode, just log or do nothing
System.Diagnostics.Debug.WriteLine($"IPC Message: {message}");
return 0;
});
#else #else
/* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */ /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */
Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true); Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true);
Exit(); Exit();
#endif #endif
} }
} }

View File

@@ -8,7 +8,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid Visibility="{x:Bind HasConflicts, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Button Click="ShortcutConflictBtn_Click" Style="{StaticResource SubtleButtonStyle}"> <Button Click="ShortcutConflictBtn_Click" Style="{StaticResource SubtleButtonStyle}">
<Grid ColumnSpacing="16"> <Grid ColumnSpacing="16">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -21,13 +21,13 @@
Foreground="{ThemeResource SystemFillColorCriticalBrush}" Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Glyph="&#xE814;" /> Glyph="&#xE814;" />
<StackPanel Grid.Column="1" Orientation="Vertical"> <StackPanel Grid.Column="1" Orientation="Vertical">
<TextBlock FontWeight="SemiBold" Text="Shortcut conflicts" /> <TextBlock x:Uid="ShortcutConflictControl_Title" FontWeight="SemiBold" />
<TextBlock <TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}" Style="{StaticResource CaptionTextBlockStyle}"
Text="2 conflicts found" /> Text="{x:Bind ConflictText, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Button> </Button>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -4,37 +4,122 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime; using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.Windows.ApplicationModel.Resources;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace Microsoft.PowerToys.Settings.UI.Controls namespace Microsoft.PowerToys.Settings.UI.Controls
{ {
public sealed partial class ShortcutConflictControl : UserControl public sealed partial class ShortcutConflictControl : UserControl, INotifyPropertyChanged
{ {
private static readonly ResourceLoader ResourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
public static readonly DependencyProperty AllHotkeyConflictsDataProperty =
DependencyProperty.Register(
nameof(AllHotkeyConflictsData),
typeof(AllHotkeyConflictsData),
typeof(ShortcutConflictControl),
new PropertyMetadata(null, OnAllHotkeyConflictsDataChanged));
public AllHotkeyConflictsData AllHotkeyConflictsData
{
get => (AllHotkeyConflictsData)GetValue(AllHotkeyConflictsDataProperty);
set => SetValue(AllHotkeyConflictsDataProperty, value);
}
public int ConflictCount
{
get
{
if (AllHotkeyConflictsData == null)
{
return 0;
}
int count = 0;
if (AllHotkeyConflictsData.InAppConflicts != null)
{
count += AllHotkeyConflictsData.InAppConflicts.Count;
}
if (AllHotkeyConflictsData.SystemConflicts != null)
{
count += AllHotkeyConflictsData.SystemConflicts.Count;
}
return count;
}
}
public string ConflictText
{
get
{
var count = ConflictCount;
return count switch
{
0 => ResourceLoader.GetString("ShortcutConflictControl_NoConflictsFound"),
1 => ResourceLoader.GetString("ShortcutConflictControl_SingleConflictFound"),
_ => string.Format(
System.Globalization.CultureInfo.CurrentCulture,
ResourceLoader.GetString("ShortcutConflictControl_MultipleConflictsFound"),
count),
};
}
}
public bool HasConflicts => ConflictCount > 0;
private static void OnAllHotkeyConflictsDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ShortcutConflictControl control)
{
control.UpdateProperties();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void UpdateProperties()
{
OnPropertyChanged(nameof(ConflictCount));
OnPropertyChanged(nameof(ConflictText));
OnPropertyChanged(nameof(HasConflicts));
// Update visibility based on conflict count
Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed;
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public ShortcutConflictControl() public ShortcutConflictControl()
{ {
InitializeComponent(); InitializeComponent();
GetShortcutConflicts(); DataContext = this;
}
private void GetShortcutConflicts() // Initially hide the control if no conflicts
{ Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed;
// TO DO: Implement the logic to retrieve and display shortcut conflicts. Make sure to Collapse this control if not conflicts are found.
} }
private void ShortcutConflictBtn_Click(object sender, RoutedEventArgs e) private void ShortcutConflictBtn_Click(object sender, RoutedEventArgs e)
{ {
// TO DO: Handle the button click event to show the shortcut conflicts window. if (AllHotkeyConflictsData == null || !HasConflicts)
{
return;
}
// Create and show the new window instead of dialog
var conflictWindow = new ShortcutConflictWindow();
// Show the window
conflictWindow.Activate();
} }
} }
} }

View File

@@ -0,0 +1,176 @@
<winuiex:WindowEx
x:Class="Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard.ShortcutConflictWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hotkeyConflicts="using:Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:winuiex="using:WinUIEx"
MinWidth="480"
MinHeight="600"
MaxWidth="900"
MaxHeight="1000"
Closed="WindowEx_Closed"
IsMaximizable="False"
IsMinimizable="False"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid x:Name="RootGrid">
<Grid.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<LinearGradientBrush x:Key="WindowsLogoGradient" StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0.0" Color="#FF80F9FF" />
<GradientStop Offset="1" Color="#FF0B9CFF" />
</LinearGradientBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<LinearGradientBrush x:Key="WindowsLogoGradient" StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0.0" Color="#FF4DD2FF" />
<GradientStop Offset="0.75" Color="#FF0078D4" />
</LinearGradientBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="WindowsLogoGradient" Color="{StaticResource SystemColorHighlightTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Title Bar Area -->
<Grid
x:Name="titleBar"
Height="48"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftPaddingColumn" Width="0" />
<ColumnDefinition x:Name="IconColumn" Width="Auto" />
<ColumnDefinition x:Name="TitleColumn" Width="Auto" />
<ColumnDefinition x:Name="RightPaddingColumn" Width="0" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="1"
Width="16"
Height="16"
VerticalAlignment="Center"
Source="/Assets/Settings/icon.ico" />
<TextBlock
x:Uid="ShortcutConflictWindow_TitleTxt"
Grid.Column="2"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" />
</Grid>
<!-- Description text -->
<TextBlock
x:Uid="ShortcutConflictWindow_Description"
Grid.Row="1"
Margin="16,24,16,24"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource BodyTextBlockStyle}"
TextWrapping="Wrap" />
<!-- Main Content Area -->
<ScrollViewer Grid.Row="2">
<Grid Margin="16,0,16,16">
<!-- Conflicts List -->
<ItemsControl x:Name="ConflictItemsControl" ItemsSource="{x:Bind ViewModel.ConflictItems, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Spacing="32" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="hotkeyConflicts:HotkeyConflictGroupData">
<StackPanel Orientation="Vertical">
<!-- Hotkey Header -->
<controls:ShortcutWithTextLabelControl
x:Uid="ShortcutConflictWindow_ModulesUsingShortcut"
Margin="0,0,0,8"
FontWeight="SemiBold"
Keys="{x:Bind Hotkey.GetKeysList()}"
LabelPlacement="Before" />
<!-- PowerToys Module Cards -->
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind Modules}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="hotkeyConflicts:ModuleHotkeyData">
<tkcontrols:SettingsCard
Margin="0,0,0,4"
Click="SettingsCard_Click"
Description="{x:Bind DisplayName}"
Header="{x:Bind Header}"
IsClickEnabled="True">
<tkcontrols:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False" UriSource="{x:Bind IconPath}" />
</tkcontrols:SettingsCard.HeaderIcon>
<!-- ShortcutControl with TwoWay binding and enabled for editing -->
<controls:ShortcutControl
x:Name="ShortcutControl"
MinWidth="140"
Margin="2"
VerticalAlignment="Center"
HasConflict="True"
HotkeySettings="{x:Bind HotkeySettings, Mode=TwoWay}"
IsEnabled="True" />
</tkcontrols:SettingsCard>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- System Conflict Card (only show if it's a system conflict) -->
<tkcontrols:SettingsCard
x:Name="SystemConflictCard"
x:Uid="ShortcutConflictWindow_SystemCard"
Visibility="{x:Bind IsSystemConflict}">
<tkcontrols:SettingsCard.HeaderIcon>
<PathIcon Data="M9 20H0V11H9V20ZM20 20H11V11H20V20ZM9 9H0V0H9V9ZM20 9H11V0H20V9Z" Foreground="{ThemeResource WindowsLogoGradient}" />
</tkcontrols:SettingsCard.HeaderIcon>
<!-- System shortcut message -->
<TextBlock
x:Uid="ShortcutConflictWindow_SystemShortcutMessage"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</tkcontrols:SettingsCard>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
<!-- Empty State (when no conflicts) -->
<StackPanel
x:Name="EmptyStatePanel"
Grid.Row="2"
Margin="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed">
<FontIcon
HorizontalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="48"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE73E;" />
<TextBlock Margin="0,16,0,4" HorizontalAlignment="Center">
<Run x:Uid="ShortcutConflictWindow_NoConflictsTitle" />
<Run x:Uid="ShortcutConflictWindow_NoConflictsDescription" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</TextBlock>
</StackPanel>
</Grid>
</winuiex:WindowEx>

View File

@@ -0,0 +1,91 @@
// 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 CommunityToolkit.WinUI.Controls;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Graphics;
using WinUIEx;
namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
{
public sealed partial class ShortcutConflictWindow : WindowEx
{
public ShortcutConflictViewModel DataContext { get; }
public ShortcutConflictViewModel ViewModel { get; private set; }
public ShortcutConflictWindow()
{
var settingsUtils = new SettingsUtils();
ViewModel = new ShortcutConflictViewModel(
settingsUtils,
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
this.Activated += Window_Activated_SetIcon;
// Set localized window title
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
this.ExtendsContentIntoTitleBar = true;
this.Title = resourceLoader.GetString("ShortcutConflictWindow_Title");
this.CenterOnScreen();
ViewModel.OnPageLoaded();
}
private void CenterOnScreen()
{
var displayArea = DisplayArea.GetFromWindowId(this.AppWindow.Id, DisplayAreaFallback.Nearest);
if (displayArea != null)
{
var windowSize = this.AppWindow.Size;
var centeredPosition = new PointInt32
{
X = (displayArea.WorkArea.Width - windowSize.Width) / 2,
Y = (displayArea.WorkArea.Height - windowSize.Height) / 2,
};
this.AppWindow.Move(centeredPosition);
}
}
private void SettingsCard_Click(object sender, RoutedEventArgs e)
{
if (sender is SettingsCard settingsCard &&
settingsCard.DataContext is ModuleHotkeyData moduleData)
{
var moduleType = moduleData.ModuleType;
NavigationService.Navigate(ModuleHelper.GetModulePageType(moduleType));
this.Close();
}
}
private void WindowEx_Closed(object sender, WindowEventArgs args)
{
ViewModel?.Dispose();
}
private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args)
{
// Set window icon
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.SetIcon("Assets\\Settings\\icon.ico");
}
}
}

View File

@@ -188,4 +188,4 @@
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls" xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="LayoutRoot" x:Name="LayoutRoot"
@@ -39,6 +40,7 @@
Content="{Binding}" Content="{Binding}"
CornerRadius="{StaticResource ControlCornerRadius}" CornerRadius="{StaticResource ControlCornerRadius}"
FontWeight="SemiBold" FontWeight="SemiBold"
IsInvalid="{Binding ElementName=LayoutRoot, Path=HasConflict}"
IsTabStop="False" IsTabStop="False"
Style="{StaticResource AccentKeyVisualStyle}" /> Style="{StaticResource AccentKeyVisualStyle}" />
</DataTemplate> </DataTemplate>

View File

@@ -3,9 +3,15 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
@@ -33,8 +39,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("Enabled", typeof(bool), typeof(ShortcutControl), null); public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("Enabled", typeof(bool), typeof(ShortcutControl), null);
public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register("HotkeySettings", typeof(HotkeySettings), typeof(ShortcutControl), null); public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register("HotkeySettings", typeof(HotkeySettings), typeof(ShortcutControl), null);
public static readonly DependencyProperty AllowDisableProperty = DependencyProperty.Register("AllowDisable", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnAllowDisableChanged)); public static readonly DependencyProperty AllowDisableProperty = DependencyProperty.Register("AllowDisable", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnAllowDisableChanged));
public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnHasConflictChanged));
public static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip", typeof(string), typeof(ShortcutControl), new PropertyMetadata(null, OnTooltipChanged));
private static ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; private static ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
@@ -58,6 +65,28 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
description.Text = text; description.Text = text;
} }
private static void OnHasConflictChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as ShortcutControl;
if (control == null)
{
return;
}
control.UpdateKeyVisualStyles();
}
private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as ShortcutControl;
if (control == null)
{
return;
}
control.UpdateTooltip();
}
private ShortcutDialogContentControl c = new ShortcutDialogContentControl(); private ShortcutDialogContentControl c = new ShortcutDialogContentControl();
private ContentDialog shortcutDialog; private ContentDialog shortcutDialog;
@@ -67,6 +96,18 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set => SetValue(AllowDisableProperty, value); set => SetValue(AllowDisableProperty, value);
} }
public bool HasConflict
{
get => (bool)GetValue(HasConflictProperty);
set => SetValue(HasConflictProperty, value);
}
public string Tooltip
{
get => (string)GetValue(TooltipProperty);
set => SetValue(TooltipProperty, value);
}
public bool Enabled public bool Enabled
{ {
get get
@@ -101,14 +142,54 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{ {
if (hotkeySettings != value) if (hotkeySettings != value)
{ {
// Unsubscribe from old settings
if (hotkeySettings != null)
{
hotkeySettings.PropertyChanged -= OnHotkeySettingsPropertyChanged;
}
hotkeySettings = value; hotkeySettings = value;
SetValue(HotkeySettingsProperty, value); SetValue(HotkeySettingsProperty, value);
// Subscribe to new settings
if (hotkeySettings != null)
{
hotkeySettings.PropertyChanged += OnHotkeySettingsPropertyChanged;
// Update UI based on conflict properties
UpdateConflictStatusFromHotkeySettings();
}
SetKeys(); SetKeys();
c.Keys = HotkeySettings.GetKeysList(); c.Keys = HotkeySettings?.GetKeysList();
} }
} }
} }
private void OnHotkeySettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(HotkeySettings.HasConflict) ||
e.PropertyName == nameof(HotkeySettings.ConflictDescription))
{
UpdateConflictStatusFromHotkeySettings();
}
}
private void UpdateConflictStatusFromHotkeySettings()
{
if (hotkeySettings != null)
{
// Update the ShortcutControl's conflict properties from HotkeySettings
HasConflict = hotkeySettings.HasConflict;
Tooltip = hotkeySettings.HasConflict ? hotkeySettings.ConflictDescription : null;
}
else
{
HasConflict = false;
Tooltip = null;
}
}
public ShortcutControl() public ShortcutControl()
{ {
InitializeComponent(); InitializeComponent();
@@ -136,6 +217,29 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
OnAllowDisableChanged(this, null); OnAllowDisableChanged(this, null);
} }
private void UpdateKeyVisualStyles()
{
if (PreviewKeysControl?.ItemsSource != null)
{
// Force refresh of the ItemsControl to update KeyVisual styles
var items = PreviewKeysControl.ItemsSource;
PreviewKeysControl.ItemsSource = null;
PreviewKeysControl.ItemsSource = items;
}
}
private void UpdateTooltip()
{
if (!string.IsNullOrEmpty(Tooltip))
{
ToolTipService.SetToolTip(EditButton, Tooltip);
}
else
{
ToolTipService.SetToolTip(EditButton, null);
}
}
private void ShortcutControl_Unloaded(object sender, RoutedEventArgs e) private void ShortcutControl_Unloaded(object sender, RoutedEventArgs e)
{ {
shortcutDialog.PrimaryButtonClick -= ShortcutDialog_PrimaryButtonClick; shortcutDialog.PrimaryButtonClick -= ShortcutDialog_PrimaryButtonClick;
@@ -147,6 +251,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
App.GetSettingsWindow().Activated -= ShortcutDialog_SettingsWindow_Activated; App.GetSettingsWindow().Activated -= ShortcutDialog_SettingsWindow_Activated;
} }
// Unsubscribe from HotkeySettings property changes
if (hotkeySettings != null)
{
hotkeySettings.PropertyChanged -= OnHotkeySettingsPropertyChanged;
}
// Dispose the HotkeySettingsControlHook object to terminate the hook threads when the textbox is unloaded // Dispose the HotkeySettingsControlHook object to terminate the hook threads when the textbox is unloaded
hook?.Dispose(); hook?.Dispose();
@@ -168,6 +278,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{ {
App.GetSettingsWindow().Activated += ShortcutDialog_SettingsWindow_Activated; App.GetSettingsWindow().Activated += ShortcutDialog_SettingsWindow_Activated;
} }
// Initialize tooltip when loaded
UpdateTooltip();
} }
private void KeyEventHandler(int key, bool matchValue, int matchValueCode) private void KeyEventHandler(int key, bool matchValue, int matchValueCode)
@@ -302,6 +415,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
KeyEventHandler(key, true, key); KeyEventHandler(key, true, key);
c.Keys = internalSettings.GetKeysList(); c.Keys = internalSettings.GetKeysList();
c.ConflictMessage = string.Empty;
c.HasConflict = false;
if (internalSettings.GetKeysList().Count == 0) if (internalSettings.GetKeysList().Count == 0)
{ {
@@ -336,12 +451,74 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
else else
{ {
EnableKeys(); EnableKeys();
if (lastValidSettings.IsValid())
{
if (string.Equals(lastValidSettings.ToString(), hotkeySettings.ToString(), StringComparison.OrdinalIgnoreCase))
{
c.HasConflict = hotkeySettings.HasConflict;
c.ConflictMessage = hotkeySettings.ConflictDescription;
}
else
{
// Check for conflicts with the new hotkey settings
CheckForConflicts(lastValidSettings);
}
}
} }
} }
c.IsWarningAltGr = internalSettings.Ctrl && internalSettings.Alt && !internalSettings.Win && (internalSettings.Code > 0); c.IsWarningAltGr = internalSettings.Ctrl && internalSettings.Alt && !internalSettings.Win && (internalSettings.Code > 0);
} }
private void CheckForConflicts(HotkeySettings settings)
{
void UpdateUIForConflict(bool hasConflict, HotkeyConflictResponse hotkeyConflictResponse)
{
DispatcherQueue.TryEnqueue(() =>
{
if (hasConflict)
{
// Build conflict message from all conflicts - only show module names
var conflictingModules = new HashSet<string>();
foreach (var conflict in hotkeyConflictResponse.AllConflicts)
{
if (!string.IsNullOrEmpty(conflict.ModuleName))
{
conflictingModules.Add(conflict.ModuleName);
}
}
if (conflictingModules.Count > 0)
{
var moduleNames = conflictingModules.ToArray();
var conflictMessage = moduleNames.Length == 1
? $"Conflict detected with {moduleNames[0]}"
: $"Conflicts detected with: {string.Join(", ", moduleNames)}";
c.ConflictMessage = conflictMessage;
}
else
{
c.ConflictMessage = "Conflict detected with unknown module";
}
c.HasConflict = true;
}
else
{
c.ConflictMessage = string.Empty;
c.HasConflict = false;
}
});
}
HotkeyConflictHelper.CheckHotkeyConflict(
settings,
ShellPage.SendDefaultIPCMessage,
UpdateUIForConflict);
}
private void EnableKeys() private void EnableKeys()
{ {
shortcutDialog.IsPrimaryButtonEnabled = true; shortcutDialog.IsPrimaryButtonEnabled = true;
@@ -416,6 +593,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
c.Keys = null; c.Keys = null;
c.Keys = HotkeySettings.GetKeysList(); c.Keys = HotkeySettings.GetKeysList();
c.HasConflict = hotkeySettings.HasConflict;
c.ConflictMessage = hotkeySettings.ConflictDescription;
// 92 means the Win key. The logic is: warning should be visible if the shortcut contains Alt AND contains Ctrl AND NOT contains Win. // 92 means the Win key. The logic is: warning should be visible if the shortcut contains Alt AND contains Ctrl AND NOT contains Win.
// Additional key must be present, as this is a valid, previously used shortcut shown at dialog open. Check for presence of non-modifier-key is not necessary therefore // Additional key must be present, as this is a valid, previously used shortcut shown at dialog open. Check for presence of non-modifier-key is not necessary therefore
c.IsWarningAltGr = c.Keys.Contains("Ctrl") && c.Keys.Contains("Alt") && !c.Keys.Contains(92); c.IsWarningAltGr = c.Keys.Contains("Ctrl") && c.Keys.Contains("Alt") && !c.Keys.Contains(92);
@@ -434,16 +614,32 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
lastValidSettings = hotkeySettings; lastValidSettings = hotkeySettings;
shortcutDialog.Hide(); shortcutDialog.Hide();
// Send RequestAllConflicts IPC to update the UI after changed hotkey settings.
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
} }
private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{ {
if (ComboIsValid(lastValidSettings)) if (ComboIsValid(lastValidSettings))
{ {
HotkeySettings = lastValidSettings with { }; if (c.HasConflict)
{
lastValidSettings = lastValidSettings with { HasConflict = true };
}
else
{
lastValidSettings = lastValidSettings with { HasConflict = false };
}
HotkeySettings = lastValidSettings;
} }
SetKeys(); SetKeys();
// Send RequestAllConflicts IPC to update the UI after changed hotkey settings.
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
shortcutDialog.Hide(); shortcutDialog.Hide();
} }
@@ -520,7 +716,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
private void SetKeys() private void SetKeys()
{ {
var keys = HotkeySettings.GetKeysList(); var keys = HotkeySettings?.GetKeysList();
if (keys != null && keys.Count > 0) if (keys != null && keys.Count > 0)
{ {

View File

@@ -63,6 +63,13 @@
IsOpen="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}" IsOpen="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}"
IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}" IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}"
Severity="Warning" /> Severity="Warning" />
<InfoBar
Title="Hotkey Conflict"
IsClosable="False"
IsOpen="{Binding ElementName=ShortcutContentControl, Path=HasConflict, Mode=OneWay}"
IsTabStop="{Binding ElementName=ShortcutContentControl, Path=HasConflict, Mode=OneWay}"
Message="{Binding ElementName=ShortcutContentControl, Path=ConflictMessage, Mode=OneWay}"
Severity="Warning" />
</Grid> </Grid>
<tk7controls:MarkdownTextBlock <tk7controls:MarkdownTextBlock
x:Uid="InvalidShortcutWarningLabel" x:Uid="InvalidShortcutWarningLabel"
@@ -71,4 +78,4 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -11,6 +11,24 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{ {
public sealed partial class ShortcutDialogContentControl : UserControl public sealed partial class ShortcutDialogContentControl : UserControl
{ {
public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List<object>), typeof(ShortcutDialogContentControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
public static readonly DependencyProperty ConflictMessageProperty = DependencyProperty.Register("ConflictMessage", typeof(string), typeof(ShortcutDialogContentControl), new PropertyMetadata(string.Empty));
public bool HasConflict
{
get => (bool)GetValue(HasConflictProperty);
set => SetValue(HasConflictProperty, value);
}
public string ConflictMessage
{
get => (string)GetValue(ConflictMessageProperty);
set => SetValue(ConflictMessageProperty, value);
}
public ShortcutDialogContentControl() public ShortcutDialogContentControl()
{ {
this.InitializeComponent(); this.InitializeComponent();
@@ -22,22 +40,16 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set { SetValue(KeysProperty, value); } set { SetValue(KeysProperty, value); }
} }
public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List<object>), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
public bool IsError public bool IsError
{ {
get => (bool)GetValue(IsErrorProperty); get => (bool)GetValue(IsErrorProperty);
set => SetValue(IsErrorProperty, value); set => SetValue(IsErrorProperty, value);
} }
public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
public bool IsWarningAltGr public bool IsWarningAltGr
{ {
get => (bool)GetValue(IsWarningAltGrProperty); get => (bool)GetValue(IsWarningAltGrProperty);
set => SetValue(IsWarningAltGrProperty, value); set => SetValue(IsWarningAltGrProperty, value);
} }
public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
} }
} }

View File

@@ -16,6 +16,7 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ItemsControl <ItemsControl
x:Name="ShortcutsControl"
VerticalAlignment="Center" VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw" AutomationProperties.AccessibilityView="Raw"
IsTabStop="False" IsTabStop="False"
@@ -32,14 +33,27 @@
AutomationProperties.AccessibilityView="Raw" AutomationProperties.AccessibilityView="Raw"
Content="{Binding}" Content="{Binding}"
FontSize="12" FontSize="12"
IsTabStop="False" /> IsTabStop="False"
Style="{StaticResource DefaultKeyVisualStyle}" />
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<tk7controls:MarkdownTextBlock <tk7controls:MarkdownTextBlock
x:Name="LabelControl"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="Transparent" Background="Transparent"
Text="{x:Bind Text}" /> Text="{x:Bind Text}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="LabelPlacementStates">
<VisualState x:Name="LabelAfter" />
<VisualState x:Name="LabelBefore">
<VisualState.Setters>
<Setter Target="LabelControl.(Grid.Column)" Value="0" />
<Setter Target="ShortcutsControl.(Grid.Column)" Value="1" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set { SetValue(TextProperty, value); } set { SetValue(TextProperty, value); }
} }
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string)));
public List<object> Keys public List<object> Keys
{ {
@@ -25,11 +25,40 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set { SetValue(KeysProperty, value); } set { SetValue(KeysProperty, value); }
} }
public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List<object>), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); public static readonly DependencyProperty KeysProperty = DependencyProperty.Register(nameof(Keys), typeof(List<object>), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string)));
public LabelPlacement LabelPlacement
{
get { return (LabelPlacement)GetValue(LabelPlacementProperty); }
set { SetValue(LabelPlacementProperty, value); }
}
public static readonly DependencyProperty LabelPlacementProperty = DependencyProperty.Register(nameof(LabelPlacement), typeof(LabelPlacement), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(defaultValue: LabelPlacement.After, OnIsLabelPlacementChanged));
public ShortcutWithTextLabelControl() public ShortcutWithTextLabelControl()
{ {
this.InitializeComponent(); this.InitializeComponent();
} }
private static void OnIsLabelPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs newValue)
{
if (d is ShortcutWithTextLabelControl labelControl)
{
if (labelControl.LabelPlacement == LabelPlacement.Before)
{
VisualStateManager.GoToState(labelControl, "LabelBefore", true);
}
else
{
VisualStateManager.GoToState(labelControl, "LabelAfter", true);
}
}
}
}
public enum LabelPlacement
{
Before,
After,
} }
} }

View File

@@ -20,33 +20,56 @@
</HyperlinkButton> </HyperlinkButton>
</StackPanel> </StackPanel>
<StackPanel <StackPanel Orientation="Vertical" Visibility="{x:Bind ShowDataDiagnosticsSetting, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
Orientation="Vertical"
Spacing="8"
Visibility="{x:Bind ShowDataDiagnosticsSetting, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock <TextBlock
x:Uid="Oobe_Overview_Telemetry_Title" x:Uid="Oobe_Overview_Telemetry_Title"
Margin="0,20,0,0" Margin="0,24,0,0"
Style="{StaticResource SubtitleTextBlockStyle}" /> Style="{StaticResource BodyStrongTextBlockStyle}" />
<TextBlock x:Uid="Oobe_Overview_Telemetry_Desc" Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> <TextBlock x:Uid="Oobe_Overview_Telemetry_Desc" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<tkcontrols:SettingsCard <tkcontrols:SettingsCard
x:Uid="Oobe_Overview_EnableDataDiagnostics" x:Uid="Oobe_Overview_EnableDataDiagnostics"
Margin="0,8,0,0"
HeaderIcon="{ui:FontIcon Glyph=&#xE9D9;}" HeaderIcon="{ui:FontIcon Glyph=&#xE9D9;}"
IsEnabled="{x:Bind ShowDataDiagnosticsSetting, Mode=OneWay}"> IsEnabled="{x:Bind ShowDataDiagnosticsSetting, Mode=OneWay}">
<tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Vertical">
<TextBlock
x:Uid="GeneralPage_EnableDataDiagnosticsText"
Style="{StaticResource SecondaryTextStyle}"
TextWrapping="WrapWholeWords" />
<HyperlinkButton
x:Uid="GeneralPage_DiagnosticsAndFeedback_Link"
Margin="0,2,0,0"
FontWeight="SemiBold"
NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation" />
<HyperlinkButton
x:Uid="Oobe_Overview_DiagnosticsAndFeedback_Settings_Link"
Margin="0,2,0,0"
Click="GeneralSettingsLaunchButton_Click"
FontWeight="SemiBold" />
</StackPanel>
</tkcontrols:SettingsCard.Description>
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind EnableDataDiagnostics, Mode=TwoWay}" /> <ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind EnableDataDiagnostics, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<HyperlinkButton <!-- Show title and description only when there are conflicts -->
x:Uid="Oobe_Overview_DiagnosticsAndFeedback_Settings_Link" <TextBlock
Margin="-8,0,0,0" x:Uid="Oobe_Overview_Hotkey_Conflict_Title"
Click="GeneralSettingsLaunchButton_Click" /> Margin="0,24,0,8"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<HyperlinkButton
x:Uid="Oobe_Overview_DiagnosticsAndFeedback_Link"
Margin="-8,0,0,0"
NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation" />
<!-- Always show shortcut status card -->
<tkcontrols:SettingsCard Description="{x:Bind ConflictDescription, Mode=OneWay}" Header="{x:Bind ConflictText, Mode=OneWay}">
<tkcontrols:SettingsCard.HeaderIcon>
<FontIcon Foreground="{x:Bind IconForeground, Mode=OneWay}" Glyph="{x:Bind IconGlyph, Mode=OneWay}" />
</tkcontrols:SettingsCard.HeaderIcon>
<!-- Only show button when there are conflicts -->
<Button
x:Uid="ResolveConflicts_Button"
Click="ShortcutConflictBtn_Click"
Visibility="{x:Bind HasConflicts, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</tkcontrols:SettingsCard>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</controls:OOBEPageControl.PageContent> </controls:OOBEPageControl.PageContent>

View File

@@ -2,21 +2,32 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.ComponentModel;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums; using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{ {
public sealed partial class OobeOverview : Page public sealed partial class OobeOverview : Page, INotifyPropertyChanged
{ {
public OobePowerToysModule ViewModel { get; set; } public OobePowerToysModule ViewModel { get; set; }
private bool _enableDataDiagnostics; private bool _enableDataDiagnostics;
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
private Windows.ApplicationModel.Resources.ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
public bool EnableDataDiagnostics public bool EnableDataDiagnostics
{ {
@@ -41,8 +52,151 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
} }
} }
public AllHotkeyConflictsData AllHotkeyConflictsData
{
get => _allHotkeyConflictsData;
set
{
if (_allHotkeyConflictsData != value)
{
_allHotkeyConflictsData = value;
OnPropertyChanged(nameof(AllHotkeyConflictsData));
OnPropertyChanged(nameof(ConflictCount));
OnPropertyChanged(nameof(ConflictText));
OnPropertyChanged(nameof(ConflictDescription));
OnPropertyChanged(nameof(HasConflicts));
OnPropertyChanged(nameof(IconGlyph));
OnPropertyChanged(nameof(IconForeground));
}
}
}
public int ConflictCount
{
get
{
if (AllHotkeyConflictsData == null)
{
return 0;
}
int count = 0;
if (AllHotkeyConflictsData.InAppConflicts != null)
{
count += AllHotkeyConflictsData.InAppConflicts.Count;
}
if (AllHotkeyConflictsData.SystemConflicts != null)
{
count += AllHotkeyConflictsData.SystemConflicts.Count;
}
return count;
}
}
public string ConflictText
{
get
{
var count = ConflictCount;
if (count == 0)
{
// Return no-conflict message
try
{
return resourceLoader.GetString("ShortcutConflictControl_NoConflictsFound");
}
catch
{
return "No conflicts found";
}
}
else if (count == 1)
{
// Try to get localized string
try
{
return resourceLoader.GetString("ShortcutConflictControl_SingleConflictFound");
}
catch
{
return "1 shortcut conflict";
}
}
else
{
// Try to get localized string
try
{
var template = resourceLoader.GetString("ShortcutConflictControl_MultipleConflictsFound");
return string.Format(System.Globalization.CultureInfo.CurrentCulture, template, count);
}
catch
{
return $"{count} shortcut conflicts";
}
}
}
}
public string ConflictDescription
{
get
{
var count = ConflictCount;
if (count == 0)
{
// Return no-conflict description
try
{
return resourceLoader.GetString("ShortcutConflictWindow_NoConflictsDescription");
}
catch
{
return "All shortcuts function correctly";
}
}
else
{
// Return conflict description
try
{
return resourceLoader.GetString("Oobe_Overview_Hotkey_Conflict_Card_Description");
}
catch
{
return "Shortcuts configured by PowerToys are conflicting";
}
}
}
}
public bool HasConflicts => ConflictCount > 0;
public string IconGlyph => HasConflicts ? "\uE814" : "\uE73E";
public SolidColorBrush IconForeground
{
get
{
if (HasConflicts)
{
// Red color for conflicts
return (SolidColorBrush)App.Current.Resources["SystemFillColorCriticalBrush"];
}
else
{
// Green color for no conflicts
return (SolidColorBrush)App.Current.Resources["SystemFillColorSuccessBrush"];
}
}
}
public bool ShowDataDiagnosticsSetting => GetIsDataDiagnosticsInfoBarEnabled(); public bool ShowDataDiagnosticsSetting => GetIsDataDiagnosticsInfoBarEnabled();
public event PropertyChangedEventHandler PropertyChanged;
private bool GetIsDataDiagnosticsInfoBarEnabled() private bool GetIsDataDiagnosticsInfoBarEnabled()
{ {
var isDataDiagnosticsGpoDisallowed = GPOWrapper.GetAllowDataDiagnosticsValue() == GpoRuleConfigured.Disabled; var isDataDiagnosticsGpoDisallowed = GPOWrapper.GetAllowDataDiagnosticsValue() == GpoRuleConfigured.Disabled;
@@ -57,7 +211,27 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
_enableDataDiagnostics = DataDiagnosticsSettings.GetEnabledValue(); _enableDataDiagnostics = DataDiagnosticsSettings.GetEnabledValue();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.Overview]); ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.Overview]);
DataContext = ViewModel; DataContext = this;
// Subscribe to hotkey conflict updates
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
GlobalHotkeyConflictManager.Instance.RequestAllConflicts();
}
}
private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
{
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
});
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
@@ -80,6 +254,18 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
ViewModel.LogOpeningSettingsEvent(); ViewModel.LogOpeningSettingsEvent();
} }
private void ShortcutConflictBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (AllHotkeyConflictsData == null || !HasConflicts)
{
return;
}
// Create and show the shortcut conflict window
var conflictWindow = new ShortcutConflictWindow();
conflictWindow.Activate();
}
protected override void OnNavigatedTo(NavigationEventArgs e) protected override void OnNavigatedTo(NavigationEventArgs e)
{ {
ViewModel.LogOpeningModuleEvent(); ViewModel.LogOpeningModuleEvent();
@@ -88,6 +274,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
protected override void OnNavigatedFrom(NavigationEventArgs e) protected override void OnNavigatedFrom(NavigationEventArgs e)
{ {
ViewModel.LogClosingModuleEvent(); ViewModel.LogClosingModuleEvent();
// Unsubscribe from conflict updates when leaving the page
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
}
} }
} }
} }

View File

@@ -11,141 +11,177 @@
Loaded="Page_Loaded" Loaded="Page_Loaded"
mc:Ignorable="d"> mc:Ignorable="d">
<!-- Main layout container -->
<Grid>
<!-- Main content grid -->
<Grid Margin="0,24,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Margin="0,24,0,0"> <!-- Compact Header overlay that covers both InfoBar and Title sections -->
<Grid.RowDefinitions> <Border
<RowDefinition Height="Auto" /> x:Name="HeaderOverlay"
<RowDefinition Height="Auto" /> Grid.Row="0"
<RowDefinition Height="Auto" /> Grid.RowSpan="2"
<RowDefinition Height="*" /> Margin="0,-24,0,0"
</Grid.RowDefinitions> VerticalAlignment="Top"
BorderThickness="0"
Canvas.ZIndex="1">
<tkcontrols:SettingsCard <Grid>
x:Name="WhatsNewDataDiagnosticsInfoBar" <Grid.RowDefinitions>
x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar" <RowDefinition Height="Auto" />
Margin="0,-24,0,0" <RowDefinition Height="Auto" />
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}" </Grid.RowDefinitions>
IsTabStop="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay}"
Visibility="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.HeaderIcon>
<FontIcon Foreground="{ThemeResource InfoBarInformationalSeverityIconBackground}" Glyph="&#xF167;" />
</tkcontrols:SettingsCard.HeaderIcon>
<tkcontrols:SettingsCard.Description>
<StackPanel>
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescText">
<Hyperlink NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar_Desc" />
</Hyperlink>
</TextBlock>
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescTextYesClicked" Visibility="Collapsed">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_InfoBar_Desc" />
<Hyperlink Click="DataDiagnostics_OpenSettings_Click">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_OpenSettings_Text" />
</Hyperlink>
</TextBlock>
</StackPanel>
</tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
x:Name="DataDiagnosticsButtonYes"
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_Yes"
Click="DataDiagnostics_InfoBar_YesNo_Click"
CommandParameter="Yes" />
<HyperlinkButton
x:Name="DataDiagnosticsButtonNo"
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_No"
Click="DataDiagnostics_InfoBar_YesNo_Click"
CommandParameter="No" />
<Button
Margin="16,0,0,0"
Click="DataDiagnostics_InfoBar_Close_Click"
Content="{ui:FontIcon Glyph=&#xE894;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
</StackPanel>
</tkcontrols:SettingsCard>
<StackPanel <tkcontrols:SettingsCard
Grid.Row="1" x:Name="WhatsNewDataDiagnosticsInfoBar"
Margin="32,16,0,16" x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar"
VerticalAlignment="Top" Grid.Row="0"
Orientation="Vertical"> Padding="12,8,12,8"
<TextBlock Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}"
x:Uid="Oobe_WhatsNew" IsTabStop="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay}"
AutomationProperties.HeadingLevel="Level1" Visibility="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
Style="{StaticResource TitleTextBlockStyle}" /> <tkcontrols:SettingsCard.HeaderIcon>
<HyperlinkButton NavigateUri="https://github.com/microsoft/PowerToys/releases" Style="{StaticResource TextButtonStyle}"> <FontIcon Foreground="{ThemeResource InfoBarInformationalSeverityIconBackground}" Glyph="&#xF167;" />
<TextBlock x:Uid="Oobe_WhatsNew_DetailedReleaseNotesLink" TextWrapping="Wrap" /> </tkcontrols:SettingsCard.HeaderIcon>
</HyperlinkButton> <tkcontrols:SettingsCard.Description>
</StackPanel> <StackPanel>
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescText">
<Hyperlink NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar_Desc" />
</Hyperlink>
</TextBlock>
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescTextYesClicked" Visibility="Collapsed">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_InfoBar_Desc" />
<Hyperlink Click="DataDiagnostics_OpenSettings_Click">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_OpenSettings_Text" />
</Hyperlink>
</TextBlock>
</StackPanel>
</tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
x:Name="DataDiagnosticsButtonYes"
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_Yes"
Click="DataDiagnostics_InfoBar_YesNo_Click"
CommandParameter="Yes" />
<HyperlinkButton
x:Name="DataDiagnosticsButtonNo"
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_No"
Click="DataDiagnostics_InfoBar_YesNo_Click"
CommandParameter="No" />
<Button
Margin="16,0,0,0"
Click="DataDiagnostics_InfoBar_Close_Click"
Content="{ui:FontIcon Glyph=&#xE894;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
</StackPanel>
</tkcontrols:SettingsCard>
<InfoBar <Grid Grid.Row="1" Margin="16,12,0,12">
x:Name="ErrorInfoBar" <StackPanel
x:Uid="Oobe_WhatsNew_LoadingError" VerticalAlignment="Top"
Grid.Row="2" Orientation="Vertical"
VerticalAlignment="Top" Spacing="4">
IsClosable="False" <TextBlock
IsTabStop="False" x:Uid="Oobe_WhatsNew"
Severity="Error"> AutomationProperties.HeadingLevel="Level1"
<InfoBar.ActionButton> Style="{StaticResource TitleTextBlockStyle}" />
<Button <HyperlinkButton NavigateUri="https://github.com/microsoft/PowerToys/releases" Style="{StaticResource TextButtonStyle}">
x:Uid="RetryBtn" <TextBlock x:Uid="Oobe_WhatsNew_DetailedReleaseNotesLink" TextWrapping="Wrap" />
HorizontalAlignment="Right" </HyperlinkButton>
Click="LoadReleaseNotes_Click"> </StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE72C;" />
<TextBlock x:Uid="RetryLabel" />
</StackPanel>
</Button>
</InfoBar.ActionButton>
</InfoBar>
<InfoBar
x:Name="ProxyWarningInfoBar"
x:Uid="Oobe_WhatsNew_ProxyAuthenticationWarning"
Grid.Row="2"
VerticalAlignment="Top"
IsClosable="False"
IsTabStop="False"
Severity="Warning">
<InfoBar.ActionButton>
<Button
x:Uid="RetryBtn"
HorizontalAlignment="Right"
Click="LoadReleaseNotes_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE72C;" />
<TextBlock x:Uid="RetryLabel" />
</StackPanel>
</Button>
</InfoBar.ActionButton>
</InfoBar>
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto"> <!-- ShortcutConflictControl positioned at the right side -->
<Grid Margin="32,24,32,24"> <controls:ShortcutConflictControl
<ProgressRing Grid.RowSpan="2"
x:Name="LoadingProgressRing" Margin="0,0,16,0"
HorizontalAlignment="Center" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
IsIndeterminate="True" AllHotkeyConflictsData="{x:Bind AllHotkeyConflictsData, Mode=OneWay}"
Visibility="Visible" /> Visibility="{x:Bind HasConflicts, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<tk7controls:MarkdownTextBlock </Grid>
x:Name="ReleaseNotesMarkdown" </Grid>
VerticalAlignment="Top" </Border>
Background="Transparent"
Header1FontSize="20" <!-- Reduced spacer for the compact header overlay -->
Header1FontWeight="SemiBold" <Grid Grid.Row="0" Height="0" />
Header1Margin="0,0,0,4" <Grid Grid.Row="1" Height="80" />
Header3FontSize="16"
Header3FontWeight="SemiBold" <InfoBar
Header4FontSize="16" x:Name="ErrorInfoBar"
Header4FontWeight="SemiBold" x:Uid="Oobe_WhatsNew_LoadingError"
HorizontalRuleMargin="24" Grid.Row="2"
LinkClicked="ReleaseNotesMarkdown_LinkClicked" VerticalAlignment="Top"
ListMargin="-18,4,0,12" IsClosable="False"
ParagraphMargin="0,0,0,0" IsTabStop="False"
TableMargin="24" Severity="Error">
Visibility="Collapsed" /> <InfoBar.ActionButton>
</Grid> <Button
</ScrollViewer> x:Uid="RetryBtn"
HorizontalAlignment="Right"
Click="LoadReleaseNotes_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE72C;" />
<TextBlock x:Uid="RetryLabel" />
</StackPanel>
</Button>
</InfoBar.ActionButton>
</InfoBar>
<InfoBar
x:Name="ProxyWarningInfoBar"
x:Uid="Oobe_WhatsNew_ProxyAuthenticationWarning"
Grid.Row="2"
VerticalAlignment="Top"
IsClosable="False"
IsTabStop="False"
Severity="Warning">
<InfoBar.ActionButton>
<Button
x:Uid="RetryBtn"
HorizontalAlignment="Right"
Click="LoadReleaseNotes_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE72C;" />
<TextBlock x:Uid="RetryLabel" />
</StackPanel>
</Button>
</InfoBar.ActionButton>
</InfoBar>
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
<Grid Margin="32,16,32,24">
<ProgressRing
x:Name="LoadingProgressRing"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsIndeterminate="True"
Visibility="Visible" />
<tk7controls:MarkdownTextBlock
x:Name="ReleaseNotesMarkdown"
VerticalAlignment="Top"
Background="Transparent"
Header1FontSize="20"
Header1FontWeight="SemiBold"
Header1Margin="0,0,0,4"
Header3FontSize="16"
Header3FontWeight="SemiBold"
Header4FontSize="16"
Header4FontWeight="SemiBold"
HorizontalRuleMargin="24"
LinkClicked="ReleaseNotesMarkdown_LinkClicked"
ListMargin="-18,4,0,12"
ParagraphMargin="0,0,0,0"
TableMargin="24"
Visibility="Collapsed" />
</Grid>
</ScrollViewer>
</Grid>
</Grid> </Grid>
</Page> </Page>

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@@ -17,9 +18,11 @@ using CommunityToolkit.WinUI.UI.Controls;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums; using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.SerializationContext; using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
@@ -27,12 +30,54 @@ using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{ {
public sealed partial class OobeWhatsNew : Page public sealed partial class OobeWhatsNew : Page, INotifyPropertyChanged
{ {
public OobePowerToysModule ViewModel { get; set; } public OobePowerToysModule ViewModel { get; set; }
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar(); public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar();
public AllHotkeyConflictsData AllHotkeyConflictsData
{
get => _allHotkeyConflictsData;
set
{
if (_allHotkeyConflictsData != value)
{
_allHotkeyConflictsData = value;
OnPropertyChanged(nameof(AllHotkeyConflictsData));
OnPropertyChanged(nameof(HasConflicts));
}
}
}
public bool HasConflicts
{
get
{
if (AllHotkeyConflictsData == null)
{
return false;
}
int count = 0;
if (AllHotkeyConflictsData.InAppConflicts != null)
{
count += AllHotkeyConflictsData.InAppConflicts.Count;
}
if (AllHotkeyConflictsData.SystemConflicts != null)
{
count += AllHotkeyConflictsData.SystemConflicts.Count;
}
return count > 0;
}
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="OobeWhatsNew"/> class. /// Initializes a new instance of the <see cref="OobeWhatsNew"/> class.
/// </summary> /// </summary>
@@ -40,7 +85,27 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{ {
this.InitializeComponent(); this.InitializeComponent();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.WhatsNew]); ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.WhatsNew]);
DataContext = ViewModel; DataContext = this;
// Subscribe to hotkey conflict updates
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
GlobalHotkeyConflictManager.Instance.RequestAllConflicts();
}
}
private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
{
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
});
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
private bool GetShowDataDiagnosticsInfoBar() private bool GetShowDataDiagnosticsInfoBar()
@@ -184,6 +249,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
protected override void OnNavigatedFrom(NavigationEventArgs e) protected override void OnNavigatedFrom(NavigationEventArgs e)
{ {
ViewModel.LogClosingModuleEvent(); ViewModel.LogClosingModuleEvent();
// Unsubscribe from conflict updates when leaving the page
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
}
} }
private void ReleaseNotesMarkdown_LinkClicked(object sender, LinkClickedEventArgs e) private void ReleaseNotesMarkdown_LinkClicked(object sender, LinkClickedEventArgs e)

View File

@@ -14,4 +14,4 @@
<MicaBackdrop /> <MicaBackdrop />
</Window.SystemBackdrop> </Window.SystemBackdrop>
<local:OobeShellPage x:Name="shellPage" /> <local:OobeShellPage x:Name="shellPage" />
</winuiex:WindowEx> </winuiex:WindowEx>

View File

@@ -31,6 +31,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.SendDefaultIPCMessage); ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
public void RefreshEnabledState() public void RefreshEnabledState()

View File

@@ -19,6 +19,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel = new AlwaysOnTopViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<AlwaysOnTopSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); ViewModel = new AlwaysOnTopViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<AlwaysOnTopSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
public void RefreshEnabledState() public void RefreshEnabledState()

View File

@@ -26,6 +26,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.SendDefaultIPCMessage, ShellPage.SendDefaultIPCMessage,
DispatcherQueue); DispatcherQueue);
DataContext = ViewModel; DataContext = ViewModel;
Loaded += (s, e) => ViewModel.OnPageLoaded();
InitializeComponent(); InitializeComponent();
} }

View File

@@ -35,6 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.SendDefaultIPCMessage); ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
/// <summary> /// <summary>

View File

@@ -19,6 +19,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel = new CropAndLockViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<CropAndLockSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); ViewModel = new CropAndLockViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<CropAndLockSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
public void RefreshEnabledState() public void RefreshEnabledState()

View File

@@ -133,7 +133,7 @@
Grid.Column="1" Grid.Column="1"
Orientation="Horizontal" Orientation="Horizontal"
Spacing="16"> Spacing="16">
<!--<controls:ShortcutConflictControl/>--> <controls:ShortcutConflictControl AllHotkeyConflictsData="{x:Bind ViewModel.AllHotkeyConflictsData, Mode=OneWay}" />
<controls:CheckUpdateControl /> <controls:CheckUpdateControl />
</StackPanel> </StackPanel>
</Grid> </Grid>

View File

@@ -39,6 +39,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel = new DashboardViewModel( ViewModel = new DashboardViewModel(
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
public void RefreshEnabledState() public void RefreshEnabledState()

View File

@@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var settingsUtils = new SettingsUtils(); var settingsUtils = new SettingsUtils();
ViewModel = new FancyZonesViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<FancyZonesSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); ViewModel = new FancyZonesViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<FancyZonesSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)

View File

@@ -26,6 +26,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
public void RefreshEnabledState() public void RefreshEnabledState()

View File

@@ -48,6 +48,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
InitializeComponent(); InitializeComponent();
this.MouseUtils_MouseJump_Panel.ViewModel = ViewModel; this.MouseUtils_MouseJump_Panel.ViewModel = ViewModel;
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
public void RefreshEnabledState() public void RefreshEnabledState()

View File

@@ -47,6 +47,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
private void OnConfigFileUpdate() private void OnConfigFileUpdate()

View File

@@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
DispatcherQueue); DispatcherQueue);
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
public void RefreshEnabledState() public void RefreshEnabledState()

View File

@@ -40,6 +40,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
PowerLauncherSettings settings = SettingsRepository<PowerLauncherSettings>.GetInstance(settingsUtils)?.SettingsConfig; PowerLauncherSettings settings = SettingsRepository<PowerLauncherSettings>.GetInstance(settingsUtils)?.SettingsConfig;
ViewModel = new PowerLauncherViewModel(settings, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SendDefaultIPCMessageTimed, App.IsDarkTheme); ViewModel = new PowerLauncherViewModel(settings, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SendDefaultIPCMessageTimed, App.IsDarkTheme);
DataContext = ViewModel; DataContext = ViewModel;
_ = Helper.GetFileWatcher(PowerLauncherSettings.ModuleName, "settings.json", () => _ = Helper.GetFileWatcher(PowerLauncherSettings.ModuleName, "settings.json", () =>
{ {
if (Environment.TickCount < _lastIPCMessageSentTick + 500) if (Environment.TickCount < _lastIPCMessageSentTick + 500)
@@ -79,6 +80,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_ApplicationName"), "application_name")); searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_ApplicationName"), "application_name"));
searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_StringInApplication"), "string_in_application")); searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_StringInApplication"), "string_in_application"));
searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_ExecutableName"), "executable_name")); searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_ExecutableName"), "executable_name"));
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)

View File

@@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.SendDefaultIPCMessage); ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
private void TextExtractor_ComboBox_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) private void TextExtractor_ComboBox_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)

View File

@@ -141,6 +141,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// NL moved navigation to general page to the moment when the window is first activated (to not make flyout window disappear) // NL moved navigation to general page to the moment when the window is first activated (to not make flyout window disappear)
// shellFrame.Navigate(typeof(GeneralPage)); // shellFrame.Navigate(typeof(GeneralPage));
IPCResponseHandleList.Add(ReceiveMessage); IPCResponseHandleList.Add(ReceiveMessage);
Services.IPCResponseService.Instance.RegisterForIPC();
SetTitleBar(); SetTitleBar();
if (_navViewParentLookup.Count > 0) if (_navViewParentLookup.Count > 0)

View File

@@ -20,6 +20,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var settingsUtils = new SettingsUtils(); var settingsUtils = new SettingsUtils();
ViewModel = new ShortcutGuideViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); ViewModel = new ShortcutGuideViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)

View File

@@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel = new WorkspacesViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<WorkspacesSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); ViewModel = new WorkspacesViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<WorkspacesSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
} }
public void RefreshEnabledState() public void RefreshEnabledState()

View File

@@ -2049,18 +2049,27 @@ Take a moment to preview the various utilities listed or view our comprehensive
<data name="Oobe_Overview_Telemetry_Desc.Text" xml:space="preserve"> <data name="Oobe_Overview_Telemetry_Desc.Text" xml:space="preserve">
<value>Diagnostics &amp; feedback helps us to improve PowerToys and keep it secure, up to date, and working as expected.</value> <value>Diagnostics &amp; feedback helps us to improve PowerToys and keep it secure, up to date, and working as expected.</value>
</data> </data>
<data name="Oobe_Overview_Hotkey_Conflict_Title.Text" xml:space="preserve">
<value>Shortcut conflict detection</value>
</data>
<data name="Oobe_Overview_Hotkey_Conflict_Card.Text" xml:space="preserve">
<value>Shortcuts configured by PowerToys are conflicting.</value>
</data>
<data name="Oobe_Overview_Hotkey_Conflict_Card_Description.Header" xml:space="preserve">
<value>Shortcuts configured by PowerToys are conflicting</value>
</data>
<data name="Oobe_Overview_Hotkey_NoConflict_Card_Header" xml:space="preserve">
<value>No conflicts found</value>
</data>
<data name="Oobe_Overview_Hotkey_NoConflict_Card_Description" xml:space="preserve">
<value>All shortcuts function correctly</value>
</data>
<data name="Oobe_Overview_DiagnosticsAndFeedback_Settings_Link.Content" xml:space="preserve"> <data name="Oobe_Overview_DiagnosticsAndFeedback_Settings_Link.Content" xml:space="preserve">
<value>View more diagnostic data settings</value> <value>View more diagnostic data settings</value>
</data> </data>
<data name="Oobe_Overview_DiagnosticsAndFeedback_Link.Content" xml:space="preserve"> <data name="Oobe_Overview_DiagnosticsAndFeedback_Link.Content" xml:space="preserve">
<value>Learn more about the information PowerToys logs &amp; how it gets used</value> <value>Learn more about the information PowerToys logs &amp; how it gets used</value>
</data> </data>
<data name="Oobe_Overview_EnableDataDiagnostics.Header" xml:space="preserve">
<value>Diagnostic data</value>
</data>
<data name="Oobe_Overview_EnableDataDiagnostics.Description" xml:space="preserve">
<value>Helps us make PowerToys faster, more stable, and better over time</value>
</data>
<data name="Oobe_WhatsNew_DataDiagnostics_InfoBar.Header" xml:space="preserve"> <data name="Oobe_WhatsNew_DataDiagnostics_InfoBar.Header" xml:space="preserve">
<value>Turn on diagnostic data to help us improve PowerToys?</value> <value>Turn on diagnostic data to help us improve PowerToys?</value>
</data> </data>
@@ -5127,6 +5136,58 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="KeyBack" xml:space="preserve"> <data name="KeyBack" xml:space="preserve">
<value>Back key</value> <value>Back key</value>
</data> </data>
<data name="InAppHotkeyConflictTooltipText" xml:space="preserve">
<value>This shortcut is already in use by another utility.</value>
</data>
<data name="SysHotkeyConflictTooltipText" xml:space="preserve">
<value>This shortcut is already in use by a default system shortcut.</value>
</data>
<data name="ShortcutConflictWindow_Title" xml:space="preserve">
<value>PowerToys shortcut conflicts</value>
</data>
<data name="ShortcutConflictWindow_TitleTxt.Text" xml:space="preserve">
<value>PowerToys shortcut conflicts</value>
</data>
<data name="ShortcutConflictWindow_Description.Text" xml:space="preserve">
<value>Conflicting shortcuts may cause unexpected behavior. Edit them here or go to the module settings to update them.</value>
</data>
<data name="ShortcutConflictWindow_ModulesUsingShortcut.Text" xml:space="preserve">
<value>Conflicts found for</value>
</data>
<data name="ShortcutConflictWindow_SystemCard.Header" xml:space="preserve">
<value>System</value>
</data>
<data name="ShortcutConflictWindow_SystemCard.Description" xml:space="preserve">
<value>Windows system shortcut</value>
</data>
<data name="ShortcutConflictWindow_SystemShortcutMessage.Text" xml:space="preserve">
<value>This shortcut can't be changed.</value>
</data>
<data name="ShortcutConflictWindow_SystemShortcutTooltip.Content" xml:space="preserve">
<value>This shortcut is used by Windows and can't be changed.</value>
</data>
<data name="ShortcutConflictWindow_NoConflictsTitle.Text" xml:space="preserve">
<value>No conflicts detected</value>
</data>
<data name="ShortcutConflictWindow_NoConflictsDescription.Text" xml:space="preserve">
<value>All shortcuts function correctly</value>
</data>
<data name="ResolveConflicts_Button.Content" xml:space="preserve">
<value>Resolve conflicts</value>
</data>
<data name="ShortcutConflictControl_Title.Text" xml:space="preserve">
<value>Shortcut conflicts</value>
</data>
<data name="ShortcutConflictControl_NoConflictsFound" xml:space="preserve">
<value>No conflicts found</value>
</data>
<data name="ShortcutConflictControl_SingleConflictFound" xml:space="preserve">
<value>1 conflict found</value>
</data>
<data name="ShortcutConflictControl_MultipleConflictsFound" xml:space="preserve">
<value>{0} conflicts found</value>
<comment>{0} is replaced with the number of conflicts</comment>
</data>
<data name="Hosts_NoLeadingSpaces.Header" xml:space="preserve"> <data name="Hosts_NoLeadingSpaces.Header" xml:space="preserve">
<value>No leading spaces</value> <value>No leading spaces</value>
</data> </data>

View File

@@ -13,8 +13,8 @@ using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Timers; using System.Timers;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -24,15 +24,16 @@ using Windows.Security.Credentials;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class AdvancedPasteViewModel : Observable, IDisposable public partial class AdvancedPasteViewModel : PageViewModelBase
{ {
private static readonly HashSet<string> WarnHotkeys = ["Ctrl + V", "Ctrl + Shift + V"]; private static readonly HashSet<string> WarnHotkeys = ["Ctrl + V", "Ctrl + Shift + V"];
private bool _disposed;
private bool disposedValue;
// Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval // Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval
private const int SaveSettingsDelayInMs = 500; private const int SaveSettingsDelayInMs = 500;
protected override string ModuleName => AdvancedPasteSettings.ModuleName;
private GeneralSettings GeneralSettingsConfig { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; }
private readonly ISettingsUtils _settingsUtils; private readonly ISettingsUtils _settingsUtils;
@@ -98,6 +99,36 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
UpdateCustomActionsCanMoveUpDown(); UpdateCustomActionsCanMoveUpDown();
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeySettings = new List<HotkeySettings>
{
PasteAsPlainTextShortcut,
AdvancedPasteUIShortcut,
PasteAsMarkdownShortcut,
PasteAsJsonShortcut,
};
foreach (var action in _additionalActions.GetAllActions())
{
if (action is AdvancedPasteAdditionalAction additionalAction)
{
hotkeySettings.Add(additionalAction.Shortcut);
}
}
// Custom actions do not have localization header, just use the action name.
foreach (var customAction in _customActions)
{
hotkeySettings.Add(customAction.Shortcut);
}
return new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = hotkeySettings.ToArray(),
};
}
private void InitializeEnabledValue() private void InitializeEnabledValue()
{ {
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAdvancedPasteEnabledValue(); _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAdvancedPasteEnabledValue();
@@ -264,9 +295,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (_advancedPasteSettings.Properties.AdvancedPasteUIShortcut != value) if (_advancedPasteSettings.Properties.AdvancedPasteUIShortcut != value)
{ {
_advancedPasteSettings.Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut; _advancedPasteSettings.Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut;
OnPropertyChanged(nameof(AdvancedPasteUIShortcut));
OnPropertyChanged(nameof(IsConflictingCopyShortcut)); OnPropertyChanged(nameof(IsConflictingCopyShortcut));
OnPropertyChanged(nameof(AdvancedPasteUIShortcut));
SaveAndNotifySettings(); SaveAndNotifySettings();
} }
} }
@@ -280,9 +310,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (_advancedPasteSettings.Properties.PasteAsPlainTextShortcut != value) if (_advancedPasteSettings.Properties.PasteAsPlainTextShortcut != value)
{ {
_advancedPasteSettings.Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut; _advancedPasteSettings.Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut;
OnPropertyChanged(nameof(PasteAsPlainTextShortcut));
OnPropertyChanged(nameof(IsConflictingCopyShortcut)); OnPropertyChanged(nameof(IsConflictingCopyShortcut));
OnPropertyChanged(nameof(PasteAsPlainTextShortcut));
SaveAndNotifySettings(); SaveAndNotifySettings();
} }
} }
@@ -296,9 +325,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (_advancedPasteSettings.Properties.PasteAsMarkdownShortcut != value) if (_advancedPasteSettings.Properties.PasteAsMarkdownShortcut != value)
{ {
_advancedPasteSettings.Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings(); _advancedPasteSettings.Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings();
OnPropertyChanged(nameof(PasteAsMarkdownShortcut));
OnPropertyChanged(nameof(IsConflictingCopyShortcut)); OnPropertyChanged(nameof(IsConflictingCopyShortcut));
OnPropertyChanged(nameof(PasteAsMarkdownShortcut));
SaveAndNotifySettings(); SaveAndNotifySettings();
} }
} }
@@ -312,9 +340,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (_advancedPasteSettings.Properties.PasteAsJsonShortcut != value) if (_advancedPasteSettings.Properties.PasteAsJsonShortcut != value)
{ {
_advancedPasteSettings.Properties.PasteAsJsonShortcut = value ?? new HotkeySettings(); _advancedPasteSettings.Properties.PasteAsJsonShortcut = value ?? new HotkeySettings();
OnPropertyChanged(nameof(PasteAsJsonShortcut));
OnPropertyChanged(nameof(IsConflictingCopyShortcut)); OnPropertyChanged(nameof(IsConflictingCopyShortcut));
OnPropertyChanged(nameof(PasteAsJsonShortcut));
SaveAndNotifySettings(); SaveAndNotifySettings();
} }
} }
@@ -399,23 +426,31 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar)); OnPropertyChanged(nameof(ShowClipboardHistoryIsGpoConfiguredInfoBar));
} }
protected virtual void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (!disposedValue) if (!_disposed)
{ {
if (disposing) if (disposing)
{ {
_delayedTimer.Dispose(); _delayedTimer?.Dispose();
foreach (var action in _additionalActions.GetAllActions())
{
action.PropertyChanged -= OnAdditionalActionPropertyChanged;
}
foreach (var customAction in _customActions)
{
customAction.PropertyChanged -= OnCustomActionPropertyChanged;
}
_customActions.CollectionChanged -= OnCustomActionsCollectionChanged;
} }
disposedValue = true; _disposed = true;
} }
}
public void Dispose() base.Dispose(disposing);
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
} }
internal void DisableAI() internal void DisableAI()

View File

@@ -3,11 +3,13 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -16,8 +18,10 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class AlwaysOnTopViewModel : Observable public partial class AlwaysOnTopViewModel : PageViewModelBase
{ {
protected override string ModuleName => AlwaysOnTopSettings.ModuleName;
private ISettingsUtils SettingsUtils { get; set; } private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; }
@@ -75,6 +79,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [Hotkey],
};
return hotkeysDict;
}
public bool IsEnabled public bool IsEnabled
{ {
get => _isEnabled; get => _isEnabled;

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
@@ -11,6 +12,7 @@ using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -21,8 +23,10 @@ using Windows.Management.Deployment;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public class CmdPalViewModel : Observable public class CmdPalViewModel : PageViewModelBase
{ {
protected override string ModuleName => "CmdPal";
private GpoRuleConfigured _enabledGpoRuleConfiguration; private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _isEnabled; private bool _isEnabled;
private HotkeySettings _hotkey; private HotkeySettings _hotkey;
@@ -88,6 +92,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [Hotkey],
};
return hotkeysDict;
}
public bool IsEnabled public bool IsEnabled
{ {
get => _isEnabled; get => _isEnabled;

View File

@@ -9,9 +9,9 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Timers; using System.Timers;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations; using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -20,9 +20,11 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class ColorPickerViewModel : Observable, IDisposable public partial class ColorPickerViewModel : PageViewModelBase
{ {
private bool disposedValue; protected override string ModuleName => ColorPickerSettings.ModuleName;
private bool _disposed;
// Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval // Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it; otherwise, we schedule saving it after this interval
private const int SaveSettingsDelayInMs = 500; private const int SaveSettingsDelayInMs = 500;
@@ -87,6 +89,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [ActivationShortcut],
};
return hotkeysDict;
}
public bool IsEnabled public bool IsEnabled
{ {
get => _isEnabled; get => _isEnabled;
@@ -409,23 +421,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(IsEnabled)); OnPropertyChanged(nameof(IsEnabled));
} }
protected virtual void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (!disposedValue) if (!_disposed)
{ {
if (disposing) if (disposing)
{ {
_delayedTimer.Dispose(); _delayedTimer?.Dispose();
foreach (var colorFormat in ColorFormats)
{
colorFormat.PropertyChanged -= ColorFormat_PropertyChanged;
}
ColorFormats.CollectionChanged -= ColorFormats_CollectionChanged;
} }
disposedValue = true; _disposed = true;
} }
}
public void Dispose() base.Dispose(disposing);
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
} }
internal ColorFormatModel GetNewColorFormatModel() internal ColorFormatModel GetNewColorFormatModel()

View File

@@ -3,11 +3,12 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -16,8 +17,10 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class CropAndLockViewModel : Observable public partial class CropAndLockViewModel : PageViewModelBase
{ {
protected override string ModuleName => CropAndLockSettings.ModuleName;
private ISettingsUtils SettingsUtils { get; set; } private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; }
@@ -66,6 +69,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [ReparentActivationShortcut, ThumbnailActivationShortcut],
};
return hotkeysDict;
}
public bool IsEnabled public bool IsEnabled
{ {
get => _isEnabled; get => _isEnabled;

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Windows.Threading; using System.Windows.Threading;
using CommunityToolkit.WinUI.Controls; using CommunityToolkit.WinUI.Controls;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
@@ -14,6 +15,7 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Microsoft.PowerToys.Settings.UI.Services; using Microsoft.PowerToys.Settings.UI.Services;
@@ -23,8 +25,10 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class DashboardViewModel : Observable public partial class DashboardViewModel : PageViewModelBase
{ {
protected override string ModuleName => "Dashboard";
private const string JsonFileType = ".json"; private const string JsonFileType = ".json";
private Dispatcher dispatcher; private Dispatcher dispatcher;
@@ -36,6 +40,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ObservableCollection<DashboardListItem> ActionModules { get; set; } = new ObservableCollection<DashboardListItem>(); public ObservableCollection<DashboardListItem> ActionModules { get; set; } = new ObservableCollection<DashboardListItem>();
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
public AllHotkeyConflictsData AllHotkeyConflictsData
{
get => _allHotkeyConflictsData;
set
{
if (Set(ref _allHotkeyConflictsData, value))
{
OnPropertyChanged();
}
}
}
public string PowerToysVersion public string PowerToysVersion
{ {
get get
@@ -66,6 +84,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GetShortcutModules(); GetShortcutModules();
} }
protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
{
dispatcher.BeginInvoke(() =>
{
AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
});
}
private void RequestConflictData()
{
// Request current conflicts data
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
}
private void AddDashboardListItem(ModuleType moduleType) private void AddDashboardListItem(ModuleType moduleType)
{ {
GpoRuleConfigured gpo = ModuleHelper.GetModuleGpoConfiguration(moduleType); GpoRuleConfigured gpo = ModuleHelper.GetModuleGpoConfiguration(moduleType);
@@ -93,6 +125,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var settings = NewPlusViewModel.LoadSettings(settingsUtils); var settings = NewPlusViewModel.LoadSettings(settingsUtils);
NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value); NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value);
} }
// Request updated conflicts after module state change
RequestConflictData();
} }
public void ModuleEnabledChangedOnSettingsPage() public void ModuleEnabledChangedOnSettingsPage()
@@ -102,6 +137,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GetShortcutModules(); GetShortcutModules();
OnPropertyChanged(nameof(ShortcutModules)); OnPropertyChanged(nameof(ShortcutModules));
// Request updated conflicts after module state change
RequestConflictData();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -3,9 +3,11 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -13,14 +15,14 @@ using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class FancyZonesViewModel : Observable public partial class FancyZonesViewModel : PageViewModelBase
{ {
private SettingsUtils SettingsUtils { get; set; } protected override string ModuleName => FancyZonesSettings.ModuleName;
private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; }
private const string ModuleName = FancyZonesSettings.ModuleName;
public ButtonClickCommand LaunchEditorEventHandler { get; set; } public ButtonClickCommand LaunchEditorEventHandler { get; set; }
private FancyZonesSettings Settings { get; set; } private FancyZonesSettings Settings { get; set; }
@@ -44,7 +46,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Positional = 2, Positional = 2,
} }
public FancyZonesViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<FancyZonesSettings> moduleSettingsRepository, Func<string, int> ipcMSGCallBackFunc, string configFileSubfolder = "") public FancyZonesViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<FancyZonesSettings> moduleSettingsRepository, Func<string, int> ipcMSGCallBackFunc, string configFileSubfolder = "")
{ {
ArgumentNullException.ThrowIfNull(settingsUtils); ArgumentNullException.ThrowIfNull(settingsUtils);
@@ -88,8 +90,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_excludedApps = Settings.Properties.FancyzonesExcludedApps.Value; _excludedApps = Settings.Properties.FancyzonesExcludedApps.Value;
_systemTheme = Settings.Properties.FancyzonesSystemTheme.Value; _systemTheme = Settings.Properties.FancyzonesSystemTheme.Value;
_showZoneNumber = Settings.Properties.FancyzonesShowZoneNumber.Value; _showZoneNumber = Settings.Properties.FancyzonesShowZoneNumber.Value;
EditorHotkey = Settings.Properties.FancyzonesEditorHotkey.Value;
_windowSwitching = Settings.Properties.FancyzonesWindowSwitching.Value; _windowSwitching = Settings.Properties.FancyzonesWindowSwitching.Value;
EditorHotkey = Settings.Properties.FancyzonesEditorHotkey.Value;
NextTabHotkey = Settings.Properties.FancyzonesNextTabHotkey.Value; NextTabHotkey = Settings.Properties.FancyzonesNextTabHotkey.Value;
PrevTabHotkey = Settings.Properties.FancyzonesPrevTabHotkey.Value; PrevTabHotkey = Settings.Properties.FancyzonesPrevTabHotkey.Value;
@@ -134,6 +137,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [EditorHotkey, NextTabHotkey, PrevTabHotkey],
};
return hotkeysDict;
}
private GpoRuleConfigured _enabledGpoRuleConfiguration; private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured; private bool _enabledStateIsGPOConfigured;
private bool _isEnabled; private bool _isEnabled;

View File

@@ -3,11 +3,12 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -15,8 +16,10 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class MeasureToolViewModel : Observable public partial class MeasureToolViewModel : PageViewModelBase
{ {
protected override string ModuleName => MeasureToolSettings.ModuleName;
private ISettingsUtils SettingsUtils { get; set; } private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; }
@@ -59,6 +62,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [ActivationShortcut],
};
return hotkeysDict;
}
public bool IsEnabled public bool IsEnabled
{ {
get => _isEnabled; get => _isEnabled;

View File

@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -14,8 +15,10 @@ using Microsoft.PowerToys.Settings.Utilities;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class MouseUtilsViewModel : Observable public partial class MouseUtilsViewModel : PageViewModelBase
{ {
protected override string ModuleName => "MouseUtils";
private ISettingsUtils SettingsUtils { get; set; } private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; }
@@ -101,7 +104,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_mousePointerCrosshairsAutoActivate = MousePointerCrosshairsSettingsConfig.Properties.AutoActivate.Value; _mousePointerCrosshairsAutoActivate = MousePointerCrosshairsSettingsConfig.Properties.AutoActivate.Value;
int isEnabled = 0; int isEnabled = 0;
NativeMethods.SystemParametersInfo(NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
_isAnimationEnabledBySystem = isEnabled != 0; _isAnimationEnabledBySystem = isEnabled != 0;
// set the callback functions value to handle outgoing IPC message. // set the callback functions value to handle outgoing IPC message.
@@ -149,6 +153,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[FindMyMouseSettings.ModuleName] = [FindMyMouseActivationShortcut],
[MouseHighlighterSettings.ModuleName] = [MouseHighlighterActivationShortcut],
[MousePointerCrosshairsSettings.ModuleName] = [MousePointerCrosshairsActivationShortcut],
[MouseJumpSettings.ModuleName] = [MouseJumpActivationShortcut],
};
return hotkeysDict;
}
public bool IsFindMyMouseEnabled public bool IsFindMyMouseEnabled
{ {
get => _isFindMyMouseEnabled; get => _isFindMyMouseEnabled;

View File

@@ -25,7 +25,7 @@ using MouseJump.Common.Models.Styles;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class MouseUtilsViewModel : Observable public partial class MouseUtilsViewModel : PageViewModelBase
{ {
private GpoRuleConfigured _jumpEnabledGpoRuleConfiguration; private GpoRuleConfigured _jumpEnabledGpoRuleConfiguration;
private bool _jumpEnabledStateIsGPOConfigured; private bool _jumpEnabledStateIsGPOConfigured;
@@ -37,6 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
ArgumentNullException.ThrowIfNull(mouseJumpSettingsRepository); ArgumentNullException.ThrowIfNull(mouseJumpSettingsRepository);
this.MouseJumpSettingsConfig = mouseJumpSettingsRepository.SettingsConfig; this.MouseJumpSettingsConfig = mouseJumpSettingsRepository.SettingsConfig;
this.MouseJumpSettingsConfig.Properties.ThumbnailSize.PropertyChanged += this.MouseJumpThumbnailSizePropertyChanged; this.MouseJumpSettingsConfig.Properties.ThumbnailSize.PropertyChanged += this.MouseJumpThumbnailSizePropertyChanged;
} }

View File

@@ -13,7 +13,6 @@ using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Helpers;
@@ -30,8 +29,10 @@ using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class MouseWithoutBordersViewModel : Observable, IDisposable public partial class MouseWithoutBordersViewModel : PageViewModelBase, IDisposable
{ {
protected override string ModuleName => MouseWithoutBordersSettings.ModuleName;
// These should be in the same order as the ComboBoxItems in MouseWithoutBordersPage.xaml switch machine shortcut options // These should be in the same order as the ComboBoxItems in MouseWithoutBordersPage.xaml switch machine shortcut options
private readonly int[] _switchBetweenMachineShortcutOptions = private readonly int[] _switchBetweenMachineShortcutOptions =
{ {
@@ -43,18 +44,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private readonly Lock _machineMatrixStringLock = new(); private readonly Lock _machineMatrixStringLock = new();
private static readonly Dictionary<SocketStatus, Brush> StatusColors = new Dictionary<SocketStatus, Brush>() private static readonly Dictionary<SocketStatus, Brush> StatusColors = new Dictionary<SocketStatus, Brush>()
{ {
{ SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0, 0x71, 0x71, 0x71)) }, { SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0, 0x71, 0x71, 0x71)) },
{ SocketStatus.Resolving, new SolidColorBrush(Colors.Yellow) }, { SocketStatus.Resolving, new SolidColorBrush(Colors.Yellow) },
{ SocketStatus.Connecting, new SolidColorBrush(Colors.Orange) }, { SocketStatus.Connecting, new SolidColorBrush(Colors.Orange) },
{ SocketStatus.Handshaking, new SolidColorBrush(Colors.Blue) }, { SocketStatus.Handshaking, new SolidColorBrush(Colors.Blue) },
{ SocketStatus.Error, new SolidColorBrush(Colors.Red) }, { SocketStatus.Error, new SolidColorBrush(Colors.Red) },
{ SocketStatus.ForceClosed, new SolidColorBrush(Colors.Purple) }, { SocketStatus.ForceClosed, new SolidColorBrush(Colors.Purple) },
{ SocketStatus.InvalidKey, new SolidColorBrush(Colors.Brown) }, { SocketStatus.InvalidKey, new SolidColorBrush(Colors.Brown) },
{ SocketStatus.Timeout, new SolidColorBrush(Colors.Pink) }, { SocketStatus.Timeout, new SolidColorBrush(Colors.Pink) },
{ SocketStatus.SendError, new SolidColorBrush(Colors.Maroon) }, { SocketStatus.SendError, new SolidColorBrush(Colors.Maroon) },
{ SocketStatus.Connected, new SolidColorBrush(Colors.Green) }, { SocketStatus.Connected, new SolidColorBrush(Colors.Green) },
}; };
private bool _connectFieldsVisible; private bool _connectFieldsVisible;
@@ -545,6 +546,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_policyDefinedIpMappingRulesIsGPOConfigured = !string.IsNullOrWhiteSpace(_policyDefinedIpMappingRulesGPOData); _policyDefinedIpMappingRulesIsGPOConfigured = !string.IsNullOrWhiteSpace(_policyDefinedIpMappingRulesGPOData);
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [
ToggleEasyMouseShortcut,
LockMachinesShortcut,
HotKeySwitch2AllPC,
ReconnectShortcut],
};
return hotkeysDict;
}
private void LoadViewModelFromSettings(MouseWithoutBordersSettings moduleSettings) private void LoadViewModelFromSettings(MouseWithoutBordersSettings moduleSettings)
{ {
ArgumentNullException.ThrowIfNull(moduleSettings); ArgumentNullException.ThrowIfNull(moduleSettings);
@@ -998,6 +1013,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
Settings.Properties.ToggleEasyMouseShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyToggleEasyMouse; Settings.Properties.ToggleEasyMouseShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyToggleEasyMouse;
NotifyPropertyChanged(); NotifyPropertyChanged();
NotifyModuleUpdatedSettings();
} }
} }
} }
@@ -1013,6 +1029,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings.Properties.LockMachineShortcut = value; Settings.Properties.LockMachineShortcut = value;
Settings.Properties.LockMachineShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyLockMachine; Settings.Properties.LockMachineShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyLockMachine;
NotifyPropertyChanged(); NotifyPropertyChanged();
NotifyModuleUpdatedSettings();
} }
} }
} }
@@ -1028,6 +1045,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings.Properties.ReconnectShortcut = value; Settings.Properties.ReconnectShortcut = value;
Settings.Properties.ReconnectShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyReconnect; Settings.Properties.ReconnectShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyReconnect;
NotifyPropertyChanged(); NotifyPropertyChanged();
NotifyModuleUpdatedSettings();
} }
} }
} }
@@ -1043,6 +1061,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings.Properties.Switch2AllPCShortcut = value; Settings.Properties.Switch2AllPCShortcut = value;
Settings.Properties.Switch2AllPCShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeySwitch2AllPC; Settings.Properties.Switch2AllPCShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeySwitch2AllPC;
NotifyPropertyChanged(); NotifyPropertyChanged();
NotifyModuleUpdatedSettings();
} }
} }
} }
@@ -1201,11 +1220,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private void NotifyModuleUpdatedSettings() private void NotifyModuleUpdatedSettings()
{ {
SendConfigMSG( SendConfigMSG(
string.Format( string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}", "{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
MouseWithoutBordersSettings.ModuleName, MouseWithoutBordersSettings.ModuleName,
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.MouseWithoutBordersSettings))); JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.MouseWithoutBordersSettings)));
} }
public void NotifyUpdatedSettings() public void NotifyUpdatedSettings()
@@ -1241,9 +1260,43 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Clipboard.SetContent(data); Clipboard.SetContent(data);
} }
public void Dispose() protected override void Dispose(bool disposing)
{ {
GC.SuppressFinalize(this); if (disposing)
{
// Cancel the cancellation token source
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
// Wait for the machine polling task to complete
try
{
_machinePollingThreadTask?.Wait(TimeSpan.FromSeconds(1));
}
catch (AggregateException)
{
// Task was cancelled, which is expected
}
// Dispose the named pipe stream
try
{
syncHelperStream?.Dispose();
}
catch (Exception ex)
{
Logger.LogError($"Error disposing sync helper stream: {ex}");
}
finally
{
syncHelperStream = null;
}
// Dispose the semaphore
_ipcSemaphore?.Dispose();
}
base.Dispose(disposing);
} }
internal void UninstallService() internal void UninstallService()

View File

@@ -0,0 +1,251 @@
// 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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Services;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public abstract class PageViewModelBase : Observable, IDisposable
{
private readonly Dictionary<string, bool> _hotkeyConflictStatus = new Dictionary<string, bool>();
private readonly Dictionary<string, string> _hotkeyConflictTooltips = new Dictionary<string, string>();
private bool _disposed;
protected abstract string ModuleName { get; }
protected PageViewModelBase()
{
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
}
}
public virtual void OnPageLoaded()
{
Debug.WriteLine($"=== PAGE LOADED: {ModuleName} ===");
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
}
/// <summary>
/// Handles updates to hotkey conflicts for the module. This method is called when the
/// <see cref="GlobalHotkeyConflictManager"/> raises the <c>ConflictsUpdated</c> event.
/// </summary>
/// <param name="sender">The source of the event, typically the <see cref="GlobalHotkeyConflictManager"/> instance.</param>
/// <param name="e">An <see cref="AllHotkeyConflictsEventArgs"/> object containing details about the hotkey conflicts.</param>
/// <remarks>
/// Derived classes can override this method to provide custom handling for hotkey conflicts.
/// Ensure that the overridden method maintains the expected behavior of processing and logging conflict data.
/// </remarks>
protected virtual void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
{
UpdateHotkeyConflictStatus(e.Conflicts);
var allHotkeySettings = GetAllHotkeySettings();
void UpdateConflictProperties()
{
if (allHotkeySettings != null)
{
foreach (KeyValuePair<string, HotkeySettings[]> kvp in allHotkeySettings)
{
var module = kvp.Key;
var hotkeySettingsList = kvp.Value;
for (int i = 0; i < hotkeySettingsList.Length; i++)
{
var key = $"{module.ToLowerInvariant()}_{i}";
hotkeySettingsList[i].HasConflict = GetHotkeyConflictStatus(key);
hotkeySettingsList[i].ConflictDescription = GetHotkeyConflictTooltip(key);
}
}
}
}
_ = Task.Run(() =>
{
try
{
var settingsWindow = App.GetSettingsWindow();
settingsWindow.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, UpdateConflictProperties);
}
catch
{
UpdateConflictProperties();
}
});
}
public virtual Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
return null;
}
protected ModuleConflictsData GetModuleRelatedConflicts(AllHotkeyConflictsData allConflicts)
{
var moduleConflicts = new ModuleConflictsData();
if (allConflicts.InAppConflicts != null)
{
foreach (var conflict in allConflicts.InAppConflicts)
{
if (IsModuleInvolved(conflict))
{
moduleConflicts.InAppConflicts.Add(conflict);
}
}
}
if (allConflicts.SystemConflicts != null)
{
foreach (var conflict in allConflicts.SystemConflicts)
{
if (IsModuleInvolved(conflict))
{
moduleConflicts.SystemConflicts.Add(conflict);
}
}
}
return moduleConflicts;
}
private void ProcessMouseUtilsConflictGroup(HotkeyConflictGroupData conflict, HashSet<string> mouseUtilsModules, bool isSysConflict)
{
// Check if any of the modules in this conflict are MouseUtils submodules
var involvedMouseUtilsModules = conflict.Modules
.Where(module => mouseUtilsModules.Contains(module.ModuleName))
.ToList();
if (involvedMouseUtilsModules.Count != 0)
{
// For each involved MouseUtils module, mark the hotkey as having a conflict
foreach (var module in involvedMouseUtilsModules)
{
string hotkeyKey = $"{module.ModuleName.ToLowerInvariant()}_{module.HotkeyID}";
_hotkeyConflictStatus[hotkeyKey] = true;
_hotkeyConflictTooltips[hotkeyKey] = isSysConflict
? ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText")
: ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText");
}
}
}
protected virtual void UpdateHotkeyConflictStatus(AllHotkeyConflictsData allConflicts)
{
_hotkeyConflictStatus.Clear();
_hotkeyConflictTooltips.Clear();
// Since MouseUtils in Settings consolidates four modules: Find My Mouse, Mouse Highlighter, Mouse Pointer Crosshairs, and Mouse Jump
// We need to handle this case separately here.
if (string.Equals(ModuleName, "MouseUtils", StringComparison.OrdinalIgnoreCase))
{
var mouseUtilsModules = new HashSet<string>
{
FindMyMouseSettings.ModuleName,
MouseHighlighterSettings.ModuleName,
MousePointerCrosshairsSettings.ModuleName,
MouseJumpSettings.ModuleName,
};
// Process in-app conflicts
foreach (var conflict in allConflicts.InAppConflicts)
{
ProcessMouseUtilsConflictGroup(conflict, mouseUtilsModules, false);
}
// Process system conflicts
foreach (var conflict in allConflicts.SystemConflicts)
{
ProcessMouseUtilsConflictGroup(conflict, mouseUtilsModules, true);
}
}
else
{
if (allConflicts.InAppConflicts.Count > 0)
{
foreach (var conflictGroup in allConflicts.InAppConflicts)
{
foreach (var conflict in conflictGroup.Modules)
{
if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase))
{
var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}";
_hotkeyConflictStatus[keyName] = true;
_hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText");
}
}
}
}
if (allConflicts.SystemConflicts.Count > 0)
{
foreach (var conflictGroup in allConflicts.SystemConflicts)
{
foreach (var conflict in conflictGroup.Modules)
{
if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase))
{
var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}";
_hotkeyConflictStatus[keyName] = true;
_hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText");
}
}
}
}
}
}
protected virtual bool GetHotkeyConflictStatus(string key)
{
return _hotkeyConflictStatus.ContainsKey(key) && _hotkeyConflictStatus[key];
}
protected virtual string GetHotkeyConflictTooltip(string key)
{
return _hotkeyConflictTooltips.TryGetValue(key, out string value) ? value : null;
}
private bool IsModuleInvolved(HotkeyConflictGroupData conflict)
{
if (conflict.Modules == null)
{
return false;
}
return conflict.Modules.Any(module =>
string.Equals(module.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase));
}
public virtual void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
}
}
_disposed = true;
}
}
}
}

View File

@@ -3,13 +3,14 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Text.Json; using System.Text.Json;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -20,10 +21,14 @@ using Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public class PeekViewModel : Observable, IDisposable public class PeekViewModel : PageViewModelBase
{ {
protected override string ModuleName => PeekSettings.ModuleName;
private bool _isEnabled; private bool _isEnabled;
private bool _disposed;
private bool _settingsUpdating; private bool _settingsUpdating;
private GeneralSettings GeneralSettingsConfig { get; set; } private GeneralSettings GeneralSettingsConfig { get; set; }
@@ -59,6 +64,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Load the application-specific settings, including preview items. // Load the application-specific settings, including preview items.
_peekSettings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekSettings.ModuleName); _peekSettings = _settingsUtils.GetSettingsOrDefault<PeekSettings>(PeekSettings.ModuleName);
_peekPreviewSettings = _settingsUtils.GetSettingsOrDefault<PeekPreviewSettings>(PeekSettings.ModuleName, PeekPreviewSettings.FileName); _peekPreviewSettings = _settingsUtils.GetSettingsOrDefault<PeekPreviewSettings>(PeekSettings.ModuleName, PeekPreviewSettings.FileName);
SetupSettingsFileWatcher(); SetupSettingsFileWatcher();
InitializeEnabledValue(); InitializeEnabledValue();
@@ -118,6 +124,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [ActivationShortcut],
};
return hotkeysDict;
}
public bool IsEnabled public bool IsEnabled
{ {
get => _isEnabled; get => _isEnabled;
@@ -302,11 +318,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(IsEnabled)); OnPropertyChanged(nameof(IsEnabled));
} }
public void Dispose() protected override void Dispose(bool disposing)
{ {
_watcher?.Dispose(); if (!_disposed)
{
if (disposing)
{
_watcher?.Dispose();
_watcher = null;
}
GC.SuppressFinalize(this); _disposed = true;
}
base.Dispose(disposing);
} }
} }
} }

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
@@ -10,9 +11,9 @@ using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using System.Windows.Input; using System.Windows.Input;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -21,7 +22,7 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext;
namespace Microsoft.PowerToys.Settings.UI.ViewModels namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
public partial class PowerLauncherViewModel : Observable public partial class PowerLauncherViewModel : PageViewModelBase, IDisposable
{ {
private int _themeIndex; private int _themeIndex;
private int _monitorPositionIndex; private int _monitorPositionIndex;
@@ -37,6 +38,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public delegate void SendCallback(PowerLauncherSettings settings); public delegate void SendCallback(PowerLauncherSettings settings);
protected override string ModuleName => PowerLauncherSettings.ModuleName;
private readonly SendCallback callback; private readonly SendCallback callback;
private readonly Func<bool> isDark; private readonly Func<bool> isDark;
@@ -122,6 +125,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [OpenPowerLauncher],
};
return hotkeysDict;
}
private void OnPluginInfoChange(object sender, PropertyChangedEventArgs e) private void OnPluginInfoChange(object sender, PropertyChangedEventArgs e)
{ {
if ( if (

Some files were not shown because too many files have changed in this diff Show More