Merge branch 'main' into niels9001/integrated-docs
Some checks failed
Spell checking / Check Spelling (push) Has been cancelled
Spell checking / Update PR (push) Has been cancelled
Spell checking / Report (Push) (push) Has been cancelled
Spell checking / Report (PR) (push) Has been cancelled

This commit is contained in:
Niels Laute 2025-08-20 17:30:55 +02:00
commit e22041a88d
150 changed files with 6586 additions and 568 deletions

View File

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

View File

@ -788,6 +788,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@ -2850,6 +2854,22 @@ Global
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -3161,6 +3181,8 @@ Global
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

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
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
To debug settings issues:

View File

@ -112,7 +112,7 @@ private:
return {};
}
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true)
{
try
{
@ -122,6 +122,7 @@ private:
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
hotkey.isShown = isShown;
return hotkey;
}
catch (...)
@ -231,8 +232,10 @@ private:
return false;
}
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue)
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true)
{
bool actionIsShown = true;
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{
return;
@ -240,9 +243,9 @@ private:
const auto action = actionValue.GetObjectW();
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown)
{
return;
actionIsShown = false;
}
if (action.HasKey(JSON_KEY_SHORTCUT))
@ -250,7 +253,7 @@ private:
const AdditionalAction additionalAction
{
actionName.c_str(),
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
};
m_additional_actions.push_back(additionalAction);
@ -259,12 +262,12 @@ private:
{
for (const auto& [subActionName, subAction] : action)
{
process_additional_action(subActionName, subAction);
process_additional_action(subActionName, subAction, actionIsShown);
}
}
}
void read_settings(PowerToysSettings::PowerToyValues& settings)
void read_settings(PowerToysSettings::PowerToyValues& settings)
{
const auto settingsObject = settings.get_raw_json();
@ -317,9 +320,21 @@ private:
{
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
for (const auto& [actionName, additionalAction] : additionalActions)
// Define the expected order to ensure consistent hotkey ID assignment
const std::vector<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)
{
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)),
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
};
const CustomAction customActionData{
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
};
m_custom_actions.push_back(customActionData);
}
m_custom_actions.push_back(customActionData);
}
}
}

View File

@ -556,6 +556,61 @@ public:
return m_enabled;
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
if (hotkeys && buffer_size >= num_hotkeys)
{
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
// Cache the raw JSON object to avoid multiple parsing
json::JsonObject root_json = values.get_raw_json();
json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
size_t hotkey_index = 0;
// Helper lambda to extract hotkey from JSON properties
auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey {
if (properties_json.HasKey(property_name))
{
try
{
json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
// Extract hotkey properties directly from JSON
bool win = hotkey_json.GetNamedBoolean(L"win", false);
bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false);
bool alt = hotkey_json.GetNamedBoolean(L"alt", false);
bool shift = hotkey_json.GetNamedBoolean(L"shift", false);
unsigned char key = static_cast<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()
{
Logger::trace(L"Starting Process to add firewall rule");

View File

@ -141,6 +141,9 @@ public partial class ListViewModel : PageViewModel, IDisposable
// see 9806fe5d8 for the last commit that had this with sections
_isFetching = true;
// Collect all the items into new viewmodels
Collection<ListItemViewModel> newViewModels = [];
try
{
// Check for cancellation before starting expensive operations
@ -151,9 +154,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
// Check for cancellation after getting items from extension
cancellationToken.ThrowIfCancellationRequested();
// Collect all the items into new viewmodels
Collection<ListItemViewModel> newViewModels = [];
// TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built.
@ -187,11 +187,22 @@ public partial class ListViewModel : PageViewModel, IDisposable
// Check for cancellation before updating the list
cancellationToken.ThrowIfCancellationRequested();
List<ListItemViewModel> removedItems = [];
lock (_listLock)
{
// Now that we have new ViewModels for everything from the
// extension, smartly update our list of VMs
ListHelpers.InPlaceUpdateList(Items, newViewModels);
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems);
// DO NOT ThrowIfCancellationRequested AFTER THIS! If you do,
// you'll clean up list items that we've now transferred into
// .Items
}
// If we removed items, we need to clean them up, to remove our event handlers
foreach (var removedItem in removedItems)
{
removedItem.SafeCleanup();
}
// TODO: Iterate over everything in Items, and prune items from the
@ -200,6 +211,15 @@ public partial class ListViewModel : PageViewModel, IDisposable
catch (OperationCanceledException)
{
// Cancellation is expected, don't treat as error
// However, if we were cancelled, we didn't actually add these items to
// our Items list. Before we release them to the GC, make sure we clean
// them up
foreach (var vm in newViewModels)
{
vm.SafeCleanup();
}
return;
}
catch (Exception ex)

View File

@ -2,6 +2,7 @@
// 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.Diagnostics.CodeAnalysis;
using System.Text.Json;
using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Templating;
@ -96,109 +97,40 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
UpdateProperty(nameof(Card));
}
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveSubmitAction))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveExecuteAction))]
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
{
// BODGY circa GH #40979
// Usually, you're supposed to try to cast the action to a specific
// type, and use those objects to get the data you need.
// However, there's something weird with AdaptiveCards and the way it
// works when we consume it when built in Release, with AOT (and
// trimming) enabled. Any sort of `action.As<IAdaptiveSubmitAction>()`
// or similar will throw a System.InvalidCastException.
//
// Instead we have this horror show.
//
// The `action.ToJson()` blob ACTUALLY CONTAINS THE `type` field, which
// we can use to determine what kind of action it is. Then we can parse
// the JSON manually based on the type.
var actionJson = action.ToJson();
if (actionJson.TryGetValue("type", out var actionTypeValue))
if (action is AdaptiveOpenUrlAction openUrlAction)
{
var actionTypeString = actionTypeValue.GetString();
Logger.LogTrace($"atString={actionTypeString}");
var actionType = actionTypeString switch
{
"Action.Submit" => ActionType.Submit,
"Action.Execute" => ActionType.Execute,
"Action.OpenUrl" => ActionType.OpenUrl,
_ => ActionType.Unsupported,
};
Logger.LogDebug($"{actionTypeString}->{actionType}");
switch (actionType)
{
case ActionType.OpenUrl:
{
HandleOpenUrlAction(action, actionJson);
}
break;
case ActionType.Submit:
case ActionType.Execute:
{
HandleSubmitAction(action, actionJson, inputs);
}
break;
default:
Logger.LogError($"{actionType} was an unexpected action `type`");
break;
}
}
else
{
Logger.LogError($"actionJson.TryGetValue(type) failed");
}
}
private void HandleOpenUrlAction(IAdaptiveActionElement action, JsonObject actionJson)
{
if (actionJson.TryGetValue("url", out var actionUrlValue))
{
var actionUrl = actionUrlValue.GetString() ?? string.Empty;
if (Uri.TryCreate(actionUrl, default(UriCreationOptions), out var uri))
{
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(uri));
}
else
{
Logger.LogError($"Failed to produce URI for {actionUrlValue}");
}
}
}
private void HandleSubmitAction(
IAdaptiveActionElement action,
JsonObject actionJson,
JsonObject inputs)
{
var dataString = string.Empty;
if (actionJson.TryGetValue("data", out var actionDataValue))
{
dataString = actionDataValue.Stringify() ?? string.Empty;
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
return;
}
var inputString = inputs.Stringify();
_ = Task.Run(() =>
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
{
try
// Get the data and inputs
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
var inputString = inputs.Stringify();
_ = Task.Run(() =>
{
var model = _formModel.Unsafe!;
if (model != null)
try
{
var result = model.SubmitForm(inputString, dataString);
Logger.LogDebug($"SubmitForm() returned {result}");
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
var model = _formModel.Unsafe!;
if (model != null)
{
var result = model.SubmitForm(inputString, dataString);
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
}
}
}
catch (Exception ex)
{
ShowException(ex);
}
});
catch (Exception ex)
{
ShowException(ex);
}
});
}
}
private static readonly string ErrorCardJson = """

View File

@ -0,0 +1,119 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class AllAppsCommandProviderTests : AppsTestBase
{
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var provider = new AllAppsCommandProvider();
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new AllAppsCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new AllAppsCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void LookupAppWithEmptyNameReturnsNotNull()
{
// Setup
var mockApp = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
MockCache.AddWin32Program(mockApp);
var page = new AllAppsPage(MockCache);
var provider = new AllAppsCommandProvider(page);
// Act
var result = provider.LookupApp(string.Empty);
// Assert
Assert.IsNotNull(result);
}
[TestMethod]
public async Task ProviderWithMockData_LookupApp_ReturnsCorrectApp()
{
// Arrange
var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
MockCache.AddWin32Program(testApp);
var provider = new AllAppsCommandProvider(Page);
// Wait for initialization to complete
await WaitForPageInitializationAsync();
// Act
var result = provider.LookupApp("TestApp");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("TestApp", result.Title);
}
[TestMethod]
public async Task ProviderWithMockData_LookupApp_ReturnsNullForNonExistentApp()
{
// Arrange
var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
MockCache.AddWin32Program(testApp);
var provider = new AllAppsCommandProvider(Page);
// Wait for initialization to complete
await WaitForPageInitializationAsync();
// Act
var result = provider.LookupApp("NonExistentApp");
// Assert
Assert.IsNull(result);
}
[TestMethod]
public void ProviderWithMockData_TopLevelCommands_IncludesListItem()
{
// Arrange
var provider = new AllAppsCommandProvider(Page);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length >= 1); // At least the list item should be present
}
}

View File

@ -0,0 +1,97 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class AllAppsPageTests : AppsTestBase
{
[TestMethod]
public void AllAppsPage_Constructor_ThrowsOnNullAppCache()
{
// Act & Assert
Assert.ThrowsException<ArgumentNullException>(() => new AllAppsPage(null!));
}
[TestMethod]
public void AllAppsPage_WithMockCache_InitializesSuccessfully()
{
// Arrange
var mockCache = new MockAppCache();
// Act
var page = new AllAppsPage(mockCache);
// Assert
Assert.IsNotNull(page);
Assert.IsNotNull(page.Name);
Assert.IsNotNull(page.Icon);
}
[TestMethod]
public async Task AllAppsPage_GetItems_ReturnsEmptyWithEmptyCache()
{
// Act - Wait for initialization to complete
await WaitForPageInitializationAsync();
var items = Page.GetItems();
// Assert
Assert.IsNotNull(items);
Assert.AreEqual(0, items.Length);
}
[TestMethod]
public async Task AllAppsPage_GetItems_ReturnsAppsFromCacheAsync()
{
// Arrange
var mockCache = new MockAppCache();
var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
mockCache.AddWin32Program(win32App);
mockCache.AddUWPApplication(uwpApp);
var page = new AllAppsPage(mockCache);
// Wait a bit for initialization to complete
await Task.Delay(100);
// Act
var items = page.GetItems();
// Assert
Assert.IsNotNull(items);
Assert.AreEqual(2, items.Length);
// we need to loop the items to ensure we got the correct ones
Assert.IsTrue(items.Any(i => i.Title == "Notepad"));
Assert.IsTrue(items.Any(i => i.Title == "Calculator"));
}
[TestMethod]
public async Task AllAppsPage_GetPinnedApps_ReturnsEmptyWhenNoAppsArePinned()
{
// Arrange
var mockCache = new MockAppCache();
var app = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
mockCache.AddWin32Program(app);
var page = new AllAppsPage(mockCache);
// Wait a bit for initialization to complete
await Task.Delay(100);
// Act
var pinnedApps = page.GetPinnedApps();
// Assert
Assert.IsNotNull(pinnedApps);
Assert.AreEqual(0, pinnedApps.Length);
}
}

View File

@ -0,0 +1,67 @@
// 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.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
/// <summary>
/// Base class for Apps unit tests that provides common setup and teardown functionality.
/// </summary>
public abstract class AppsTestBase
{
/// <summary>
/// Gets the mock application cache used in tests.
/// </summary>
protected MockAppCache MockCache { get; private set; } = null!;
/// <summary>
/// Gets the AllAppsPage instance used in tests.
/// </summary>
protected AllAppsPage Page { get; private set; } = null!;
/// <summary>
/// Sets up the test environment before each test method.
/// </summary>
/// <returns>A task representing the asynchronous setup operation.</returns>
[TestInitialize]
public virtual async Task Setup()
{
MockCache = new MockAppCache();
Page = new AllAppsPage(MockCache);
// Ensure initialization is complete
await MockCache.RefreshAsync();
}
/// <summary>
/// Cleans up the test environment after each test method.
/// </summary>
[TestCleanup]
public virtual void Cleanup()
{
MockCache?.Dispose();
}
/// <summary>
/// Forces synchronous initialization of the page for testing.
/// </summary>
protected void EnsurePageInitialized()
{
// Trigger BuildListItems by accessing items
_ = Page.GetItems();
}
/// <summary>
/// Waits for page initialization with timeout.
/// </summary>
/// <param name="timeoutMs">The timeout in milliseconds.</param>
/// <returns>A task representing the asynchronous wait operation.</returns>
protected async Task WaitForPageInitializationAsync(int timeoutMs = 1000)
{
await MockCache.RefreshAsync();
EnsurePageInitialized();
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.Apps.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,113 @@
// 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.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
/// <summary>
/// Mock implementation of IAppCache for unit testing.
/// </summary>
public class MockAppCache : IAppCache
{
private readonly List<Win32Program> _win32s = new();
private readonly List<IUWPApplication> _uwps = new();
private bool _disposed;
private bool _shouldReload;
/// <summary>
/// Gets the collection of Win32 programs.
/// </summary>
public IList<Win32Program> Win32s => _win32s.AsReadOnly();
/// <summary>
/// Gets the collection of UWP applications.
/// </summary>
public IList<IUWPApplication> UWPs => _uwps.AsReadOnly();
/// <summary>
/// Determines whether the cache should be reloaded.
/// </summary>
/// <returns>True if cache should be reloaded, false otherwise.</returns>
public bool ShouldReload() => _shouldReload;
/// <summary>
/// Resets the reload flag.
/// </summary>
public void ResetReloadFlag() => _shouldReload = false;
/// <summary>
/// Asynchronously refreshes the cache.
/// </summary>
/// <returns>A task representing the asynchronous refresh operation.</returns>
public async Task RefreshAsync()
{
// Simulate minimal async operation for testing
await Task.Delay(1);
}
/// <summary>
/// Adds a Win32 program to the cache.
/// </summary>
/// <param name="program">The Win32 program to add.</param>
/// <exception cref="ArgumentNullException">Thrown when program is null.</exception>
public void AddWin32Program(Win32Program program)
{
ArgumentNullException.ThrowIfNull(program);
_win32s.Add(program);
}
/// <summary>
/// Adds a UWP application to the cache.
/// </summary>
/// <param name="app">The UWP application to add.</param>
/// <exception cref="ArgumentNullException">Thrown when app is null.</exception>
public void AddUWPApplication(IUWPApplication app)
{
ArgumentNullException.ThrowIfNull(app);
_uwps.Add(app);
}
/// <summary>
/// Clears all applications from the cache.
/// </summary>
public void ClearAll()
{
_win32s.Clear();
_uwps.Clear();
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Clean up managed resources
_win32s.Clear();
_uwps.Clear();
}
_disposed = true;
}
}
}

View File

@ -0,0 +1,140 @@
// 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 Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
/// <summary>
/// Mock implementation of IUWPApplication for unit testing.
/// </summary>
public class MockUWPApplication : IUWPApplication
{
/// <summary>
/// Gets or sets the app list entry.
/// </summary>
public string AppListEntry { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the unique identifier.
/// </summary>
public string UniqueIdentifier { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the display name.
/// </summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the description.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the user model ID.
/// </summary>
public string UserModelId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the background color.
/// </summary>
public string BackgroundColor { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the entry point.
/// </summary>
public string EntryPoint { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the application is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the application can run elevated.
/// </summary>
public bool CanRunElevated { get; set; }
/// <summary>
/// Gets or sets the logo path.
/// </summary>
public string LogoPath { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the logo type.
/// </summary>
public LogoType LogoType { get; set; } = LogoType.Colored;
/// <summary>
/// Gets or sets the UWP package.
/// </summary>
public UWP Package { get; set; } = null!;
/// <summary>
/// Gets the name of the application.
/// </summary>
public string Name => DisplayName;
/// <summary>
/// Gets the location of the application.
/// </summary>
public string Location => Package?.Location ?? string.Empty;
/// <summary>
/// Gets the localized location of the application.
/// </summary>
public string LocationLocalized => Package?.LocationLocalized ?? string.Empty;
/// <summary>
/// Gets the application identifier.
/// </summary>
/// <returns>The user model ID of the application.</returns>
public string GetAppIdentifier()
{
return UserModelId;
}
/// <summary>
/// Gets the commands available for this application.
/// </summary>
/// <returns>A list of context items.</returns>
public List<IContextItem> GetCommands()
{
return new List<IContextItem>();
}
/// <summary>
/// Updates the logo path based on the specified theme.
/// </summary>
/// <param name="theme">The theme to use for the logo.</param>
public void UpdateLogoPath(Theme theme)
{
// Mock implementation - no-op for testing
}
/// <summary>
/// Converts this UWP application to an AppItem.
/// </summary>
/// <returns>An AppItem representation of this UWP application.</returns>
public AppItem ToAppItem()
{
var iconPath = LogoType != LogoType.Error ? LogoPath : string.Empty;
return new AppItem()
{
Name = Name,
Subtitle = Description,
Type = "Packaged Application", // Equivalent to UWPApplication.Type()
IcoPath = iconPath,
DirPath = Location,
UserModelId = UserModelId,
IsPackaged = true,
Commands = GetCommands(),
AppIdentifier = GetAppIdentifier(),
};
}
}

View File

@ -0,0 +1,45 @@
// 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.Linq;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[TestMethod]
public void QueryReturnsExpectedResults()
{
// Arrange
var mockCache = new MockAppCache();
var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
mockCache.AddWin32Program(win32App);
mockCache.AddUWPApplication(uwpApp);
for (var i = 0; i < 10; i++)
{
mockCache.AddWin32Program(TestDataHelper.CreateTestWin32Program($"App{i}"));
mockCache.AddUWPApplication(TestDataHelper.CreateTestUWPApplication($"UWP App {i}"));
}
var page = new AllAppsPage(mockCache);
var provider = new AllAppsCommandProvider(page);
// Act
var allItems = page.GetItems();
// Assert
var notepadResult = Query("notepad", allItems).FirstOrDefault();
Assert.IsNotNull(notepadResult);
Assert.AreEqual("Notepad", notepadResult.Title);
var calculatorResult = Query("cal", allItems).FirstOrDefault();
Assert.IsNotNull(calculatorResult);
Assert.AreEqual("Calculator", calculatorResult.Title);
}
}

View File

@ -0,0 +1,58 @@
// 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 Microsoft.CmdPal.Ext.Apps.Helpers;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
public class Settings : ISettingsInterface
{
private readonly bool enableStartMenuSource;
private readonly bool enableDesktopSource;
private readonly bool enableRegistrySource;
private readonly bool enablePathEnvironmentVariableSource;
private readonly List<string> programSuffixes;
private readonly List<string> runCommandSuffixes;
public Settings(
bool enableStartMenuSource = true,
bool enableDesktopSource = true,
bool enableRegistrySource = true,
bool enablePathEnvironmentVariableSource = true,
List<string> programSuffixes = null,
List<string> runCommandSuffixes = null)
{
this.enableStartMenuSource = enableStartMenuSource;
this.enableDesktopSource = enableDesktopSource;
this.enableRegistrySource = enableRegistrySource;
this.enablePathEnvironmentVariableSource = enablePathEnvironmentVariableSource;
this.programSuffixes = programSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url" };
this.runCommandSuffixes = runCommandSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url", "cpl", "msc" };
}
public bool EnableStartMenuSource => enableStartMenuSource;
public bool EnableDesktopSource => enableDesktopSource;
public bool EnableRegistrySource => enableRegistrySource;
public bool EnablePathEnvironmentVariableSource => enablePathEnvironmentVariableSource;
public List<string> ProgramSuffixes => programSuffixes;
public List<string> RunCommandSuffixes => runCommandSuffixes;
public static Settings CreateDefaultSettings() => new Settings();
public static Settings CreateDisabledSourcesSettings() => new Settings(
enableStartMenuSource: false,
enableDesktopSource: false,
enableRegistrySource: false,
enablePathEnvironmentVariableSource: false);
public static Settings CreateCustomSuffixesSettings() => new Settings(
programSuffixes: new List<string> { "exe", "bat" },
runCommandSuffixes: new List<string> { "exe", "bat", "cmd" });
}

View File

@ -0,0 +1,128 @@
// 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 Microsoft.CmdPal.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
/// <summary>
/// Helper class to create test data for unit tests.
/// </summary>
public static class TestDataHelper
{
/// <summary>
/// Creates a test Win32 program with the specified parameters.
/// </summary>
/// <param name="name">The name of the application.</param>
/// <param name="fullPath">The full path to the application executable.</param>
/// <param name="enabled">A value indicating whether the application is enabled.</param>
/// <param name="valid">A value indicating whether the application is valid.</param>
/// <returns>A new Win32Program instance with the specified parameters.</returns>
public static Win32Program CreateTestWin32Program(
string name = "Test App",
string fullPath = "C:\\TestApp\\app.exe",
bool enabled = true,
bool valid = true)
{
return new Win32Program
{
Name = name,
FullPath = fullPath,
Enabled = enabled,
Valid = valid,
UniqueIdentifier = $"win32_{name}",
Description = $"Test description for {name}",
ExecutableName = "app.exe",
ParentDirectory = "C:\\TestApp",
AppType = Win32Program.ApplicationType.Win32Application,
};
}
/// <summary>
/// Creates a test UWP application with the specified parameters.
/// </summary>
/// <param name="displayName">The display name of the application.</param>
/// <param name="userModelId">The user model ID of the application.</param>
/// <param name="enabled">A value indicating whether the application is enabled.</param>
/// <returns>A new IUWPApplication instance with the specified parameters.</returns>
public static IUWPApplication CreateTestUWPApplication(
string displayName = "Test UWP App",
string userModelId = "TestPublisher.TestUWPApp_1.0.0.0_neutral__8wekyb3d8bbwe",
bool enabled = true)
{
return new MockUWPApplication
{
DisplayName = displayName,
UserModelId = userModelId,
Enabled = enabled,
UniqueIdentifier = $"uwp_{userModelId}",
Description = $"Test UWP description for {displayName}",
AppListEntry = "default",
BackgroundColor = "#000000",
EntryPoint = "TestApp.App",
CanRunElevated = false,
LogoPath = string.Empty,
Package = CreateMockUWPPackage(displayName, userModelId),
};
}
/// <summary>
/// Creates a mock UWP package for testing purposes.
/// </summary>
/// <param name="displayName">The display name of the package.</param>
/// <param name="userModelId">The user model ID of the package.</param>
/// <returns>A new UWP package instance.</returns>
private static UWP CreateMockUWPPackage(string displayName, string userModelId)
{
var mockPackage = new MockPackage
{
Name = displayName,
FullName = userModelId,
FamilyName = $"{displayName}_8wekyb3d8bbwe",
InstalledLocation = $"C:\\Program Files\\WindowsApps\\{displayName}",
};
return new UWP(mockPackage)
{
Location = mockPackage.InstalledLocation,
LocationLocalized = mockPackage.InstalledLocation,
};
}
/// <summary>
/// Mock implementation of IPackage for testing purposes.
/// </summary>
private sealed class MockPackage : IPackage
{
/// <summary>
/// Gets or sets the name of the package.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the full name of the package.
/// </summary>
public string FullName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the family name of the package.
/// </summary>
public string FamilyName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the package is a framework package.
/// </summary>
public bool IsFramework { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the package is in development mode.
/// </summary>
public bool IsDevelopmentMode { get; set; }
/// <summary>
/// Gets or sets the installed location of the package.
/// </summary>
public string InstalledLocation { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,42 @@
// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarkDataTests
{
[TestMethod]
public void BookmarkDataWebUrlDetection()
{
// Act
var webBookmark = new BookmarkData
{
Name = "Test Site",
Bookmark = "https://test.com",
};
var nonWebBookmark = new BookmarkData
{
Name = "Local File",
Bookmark = "C:\\temp\\file.txt",
};
var placeholderBookmark = new BookmarkData
{
Name = "Placeholder",
Bookmark = "{Placeholder}",
};
// Assert
Assert.IsTrue(webBookmark.IsWebUrl());
Assert.IsFalse(webBookmark.IsPlaceholder);
Assert.IsFalse(nonWebBookmark.IsWebUrl());
Assert.IsFalse(nonWebBookmark.IsPlaceholder);
Assert.IsTrue(placeholderBookmark.IsPlaceholder);
}
}

View File

@ -0,0 +1,535 @@
// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarkJsonParserTests
{
private BookmarkJsonParser _parser;
[TestInitialize]
public void Setup()
{
_parser = new BookmarkJsonParser();
}
[TestMethod]
public void ParseBookmarks_ValidJson_ReturnsBookmarks()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "Google",
"Bookmark": "https://www.google.com"
},
{
"Name": "Local File",
"Bookmark": "C:\\temp\\file.txt"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Data.Count);
Assert.AreEqual("Google", result.Data[0].Name);
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
Assert.AreEqual("Local File", result.Data[1].Name);
Assert.AreEqual("C:\\temp\\file.txt", result.Data[1].Bookmark);
}
[TestMethod]
public void ParseBookmarks_EmptyJson_ReturnsEmptyBookmarks()
{
// Arrange
var json = "{}";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_NullJson_ReturnsEmptyBookmarks()
{
// Act
var result = _parser.ParseBookmarks(null);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_WhitespaceJson_ReturnsEmptyBookmarks()
{
// Act
var result = _parser.ParseBookmarks(" ");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_EmptyString_ReturnsEmptyBookmarks()
{
// Act
var result = _parser.ParseBookmarks(string.Empty);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_InvalidJson_ReturnsEmptyBookmarks()
{
// Arrange
var invalidJson = "{invalid json}";
// Act
var result = _parser.ParseBookmarks(invalidJson);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_MalformedJson_ReturnsEmptyBookmarks()
{
// Arrange
var malformedJson = """
{
"Data": [
{
"Name": "Google",
"Bookmark": "https://www.google.com"
},
{
"Name": "Incomplete entry"
""";
// Act
var result = _parser.ParseBookmarks(malformedJson);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_JsonWithTrailingCommas_ParsesSuccessfully()
{
// Arrange - JSON with trailing commas (should be handled by AllowTrailingCommas option)
var json = """
{
"Data": [
{
"Name": "Google",
"Bookmark": "https://www.google.com",
},
{
"Name": "Local File",
"Bookmark": "C:\\temp\\file.txt",
},
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Data.Count);
Assert.AreEqual("Google", result.Data[0].Name);
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
}
[TestMethod]
public void ParseBookmarks_JsonWithDifferentCasing_ParsesSuccessfully()
{
// Arrange - JSON with different property name casing (should be handled by PropertyNameCaseInsensitive option)
var json = """
{
"data": [
{
"name": "Google",
"bookmark": "https://www.google.com"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Data.Count);
Assert.AreEqual("Google", result.Data[0].Name);
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
}
[TestMethod]
public void SerializeBookmarks_ValidBookmarks_ReturnsJsonString()
{
// Arrange
var bookmarks = new Bookmarks
{
Data = new List<BookmarkData>
{
new BookmarkData { Name = "Google", Bookmark = "https://www.google.com" },
new BookmarkData { Name = "Local File", Bookmark = "C:\\temp\\file.txt" },
},
};
// Act
var result = _parser.SerializeBookmarks(bookmarks);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("Google"));
Assert.IsTrue(result.Contains("https://www.google.com"));
Assert.IsTrue(result.Contains("Local File"));
Assert.IsTrue(result.Contains("C:\\\\temp\\\\file.txt")); // Escaped backslashes in JSON
Assert.IsTrue(result.Contains("Data"));
}
[TestMethod]
public void SerializeBookmarks_EmptyBookmarks_ReturnsValidJson()
{
// Arrange
var bookmarks = new Bookmarks();
// Act
var result = _parser.SerializeBookmarks(bookmarks);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("Data"));
Assert.IsTrue(result.Contains("[]"));
}
[TestMethod]
public void SerializeBookmarks_NullBookmarks_ReturnsEmptyString()
{
// Act
var result = _parser.SerializeBookmarks(null);
// Assert
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void ParseBookmarks_RoundTripSerialization_PreservesData()
{
// Arrange
var originalBookmarks = new Bookmarks
{
Data = new List<BookmarkData>
{
new BookmarkData { Name = "Google", Bookmark = "https://www.google.com" },
new BookmarkData { Name = "Local File", Bookmark = "C:\\temp\\file.txt" },
new BookmarkData { Name = "Placeholder", Bookmark = "Open {file} in editor" },
},
};
// Act - Serialize then parse
var serializedJson = _parser.SerializeBookmarks(originalBookmarks);
var parsedBookmarks = _parser.ParseBookmarks(serializedJson);
// Assert
Assert.IsNotNull(parsedBookmarks);
Assert.AreEqual(originalBookmarks.Data.Count, parsedBookmarks.Data.Count);
for (var i = 0; i < originalBookmarks.Data.Count; i++)
{
Assert.AreEqual(originalBookmarks.Data[i].Name, parsedBookmarks.Data[i].Name);
Assert.AreEqual(originalBookmarks.Data[i].Bookmark, parsedBookmarks.Data[i].Bookmark);
Assert.AreEqual(originalBookmarks.Data[i].IsPlaceholder, parsedBookmarks.Data[i].IsPlaceholder);
}
}
[TestMethod]
public void ParseBookmarks_JsonWithPlaceholderBookmarks_CorrectlyIdentifiesPlaceholders()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "Regular URL",
"Bookmark": "https://www.google.com"
},
{
"Name": "Placeholder Command",
"Bookmark": "notepad {file}"
},
{
"Name": "Multiple Placeholders",
"Bookmark": "copy {source} {destination}"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Data.Count);
Assert.IsFalse(result.Data[0].IsPlaceholder);
Assert.IsTrue(result.Data[1].IsPlaceholder);
Assert.IsTrue(result.Data[2].IsPlaceholder);
}
[TestMethod]
public void ParseBookmarks_IsWebUrl_CorrectlyIdentifiesWebUrls()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "HTTPS Website",
"Bookmark": "https://www.google.com"
},
{
"Name": "HTTP Website",
"Bookmark": "http://example.com"
},
{
"Name": "Website without protocol",
"Bookmark": "www.github.com"
},
{
"Name": "Local File Path",
"Bookmark": "C:\\Users\\test\\Documents\\file.txt"
},
{
"Name": "Network Path",
"Bookmark": "\\\\server\\share\\file.txt"
},
{
"Name": "Executable",
"Bookmark": "notepad.exe"
},
{
"Name": "File URI",
"Bookmark": "file:///C:/temp/file.txt"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(7, result.Data.Count);
// Web URLs should return true
Assert.IsTrue(result.Data[0].IsWebUrl(), "HTTPS URL should be identified as web URL");
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTP URL should be identified as web URL");
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
// Assert.IsTrue(result.Data[2].IsWebUrl(), "Domain without protocol should be identified as web URL");
// Non-web URLs should return false
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file path should not be identified as web URL");
Assert.IsFalse(result.Data[4].IsWebUrl(), "Network path should not be identified as web URL");
Assert.IsFalse(result.Data[5].IsWebUrl(), "Executable should not be identified as web URL");
Assert.IsFalse(result.Data[6].IsWebUrl(), "File URI should not be identified as web URL");
}
[TestMethod]
public void ParseBookmarks_IsPlaceholder_CorrectlyIdentifiesPlaceholders()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "Simple Placeholder",
"Bookmark": "notepad {file}"
},
{
"Name": "Multiple Placeholders",
"Bookmark": "copy {source} to {destination}"
},
{
"Name": "Web URL with Placeholder",
"Bookmark": "https://search.com?q={query}"
},
{
"Name": "Complex Placeholder",
"Bookmark": "cmd /c echo {message} > {output_file}"
},
{
"Name": "No Placeholder - Regular URL",
"Bookmark": "https://www.google.com"
},
{
"Name": "No Placeholder - Local File",
"Bookmark": "C:\\temp\\file.txt"
},
{
"Name": "False Positive - Only Opening Brace",
"Bookmark": "test { incomplete"
},
{
"Name": "False Positive - Only Closing Brace",
"Bookmark": "test } incomplete"
},
{
"Name": "Empty Placeholder",
"Bookmark": "command {}"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(9, result.Data.Count);
// Should be identified as placeholders
Assert.IsTrue(result.Data[0].IsPlaceholder, "Simple placeholder should be identified");
Assert.IsTrue(result.Data[1].IsPlaceholder, "Multiple placeholders should be identified");
Assert.IsTrue(result.Data[2].IsPlaceholder, "Web URL with placeholder should be identified");
Assert.IsTrue(result.Data[3].IsPlaceholder, "Complex placeholder should be identified");
Assert.IsTrue(result.Data[8].IsPlaceholder, "Empty placeholder should be identified");
// Should NOT be identified as placeholders
Assert.IsFalse(result.Data[4].IsPlaceholder, "Regular URL should not be placeholder");
Assert.IsFalse(result.Data[5].IsPlaceholder, "Local file should not be placeholder");
Assert.IsFalse(result.Data[6].IsPlaceholder, "Only opening brace should not be placeholder");
Assert.IsFalse(result.Data[7].IsPlaceholder, "Only closing brace should not be placeholder");
}
[TestMethod]
public void ParseBookmarks_MixedProperties_CorrectlyIdentifiesBothWebUrlAndPlaceholder()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "Web URL with Placeholder",
"Bookmark": "https://google.com/search?q={query}"
},
{
"Name": "Web URL without Placeholder",
"Bookmark": "https://github.com"
},
{
"Name": "Local File with Placeholder",
"Bookmark": "notepad {file}"
},
{
"Name": "Local File without Placeholder",
"Bookmark": "C:\\Windows\\notepad.exe"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(4, result.Data.Count);
// Web URL with placeholder
Assert.IsTrue(result.Data[0].IsWebUrl(), "Web URL with placeholder should be identified as web URL");
Assert.IsTrue(result.Data[0].IsPlaceholder, "Web URL with placeholder should be identified as placeholder");
// Web URL without placeholder
Assert.IsTrue(result.Data[1].IsWebUrl(), "Web URL without placeholder should be identified as web URL");
Assert.IsFalse(result.Data[1].IsPlaceholder, "Web URL without placeholder should not be identified as placeholder");
// Local file with placeholder
Assert.IsFalse(result.Data[2].IsWebUrl(), "Local file with placeholder should not be identified as web URL");
Assert.IsTrue(result.Data[2].IsPlaceholder, "Local file with placeholder should be identified as placeholder");
// Local file without placeholder
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file without placeholder should not be identified as web URL");
Assert.IsFalse(result.Data[3].IsPlaceholder, "Local file without placeholder should not be identified as placeholder");
}
[TestMethod]
public void ParseBookmarks_EdgeCaseUrls_CorrectlyIdentifiesWebUrls()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "FTP URL",
"Bookmark": "ftp://files.example.com"
},
{
"Name": "HTTPS with port",
"Bookmark": "https://localhost:8080"
},
{
"Name": "IP Address",
"Bookmark": "http://192.168.1.1"
},
{
"Name": "Subdomain",
"Bookmark": "https://api.github.com"
},
{
"Name": "Domain only",
"Bookmark": "example.com"
},
{
"Name": "Not a URL - no dots",
"Bookmark": "localhost"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(6, result.Data.Count);
Assert.IsFalse(result.Data[0].IsWebUrl(), "FTP URL should not be identified as web URL");
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTPS with port should be identified as web URL");
Assert.IsTrue(result.Data[2].IsWebUrl(), "IP Address with HTTP should be identified as web URL");
Assert.IsTrue(result.Data[3].IsWebUrl(), "Subdomain should be identified as web URL");
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
// Assert.IsTrue(result.Data[4].IsWebUrl(), "Domain only should be identified as web URL");
Assert.IsFalse(result.Data[5].IsWebUrl(), "Single word without dots should not be identified as web URL");
}
}

View File

@ -0,0 +1,137 @@
// 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 System.Linq;
using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarksCommandProviderTests
{
[TestMethod]
public void ProviderHasCorrectId()
{
// Setup
var mockDataSource = new MockBookmarkDataSource();
var provider = new BookmarksCommandProvider(mockDataSource);
// Assert
Assert.AreEqual("Bookmarks", provider.Id);
}
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var mockDataSource = new MockBookmarkDataSource();
var provider = new BookmarksCommandProvider(mockDataSource);
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new BookmarksCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new BookmarksCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void ProviderWithMockData_LoadsBookmarksCorrectly()
{
// Arrange
var jsonData = @"{
""Data"": [
{
""Name"": ""Test Bookmark"",
""Bookmark"": ""https://test.com""
},
{
""Name"": ""Another Bookmark"",
""Bookmark"": ""https://another.com""
}
]
}";
var dataSource = new MockBookmarkDataSource(jsonData);
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
var testBookmark = commands.Where(c => c.Title.Contains("Test Bookmark")).FirstOrDefault();
// Should have three commandsAdd + two custom bookmarks
Assert.AreEqual(3, commands.Length);
Assert.IsNotNull(addCommand);
Assert.IsNotNull(testBookmark);
}
[TestMethod]
public void ProviderWithEmptyData_HasOnlyAddCommand()
{
// Arrange
var dataSource = new MockBookmarkDataSource(@"{ ""Data"": [] }");
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
// Only have Add command
Assert.AreEqual(1, commands.Length);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
Assert.IsNotNull(addCommand);
}
[TestMethod]
public void ProviderWithInvalidData_HandlesGracefully()
{
// Arrange
var dataSource = new MockBookmarkDataSource("invalid json");
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
// Only have one command. Will ignore json parse error.
Assert.AreEqual(1, commands.Length);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
Assert.IsNotNull(addCommand);
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
// 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.
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
internal sealed class MockBookmarkDataSource : IBookmarkDataSource
{
private string _jsonData;
public MockBookmarkDataSource(string initialJsonData = "[]")
{
_jsonData = initialJsonData;
}
public string GetBookmarkData()
{
return _jsonData;
}
public void SaveBookmarkData(string jsonData)
{
_jsonData = jsonData;
}
}

View File

@ -0,0 +1,55 @@
// 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.Linq;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[TestMethod]
public void ValidateBookmarksCreation()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
// Assert
Assert.IsNotNull(bookmarks);
Assert.IsNotNull(bookmarks.Data);
Assert.AreEqual(2, bookmarks.Data.Count);
}
[TestMethod]
public void ValidateBookmarkData()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
// Act
var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
var githubBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "GitHub");
// Assert
Assert.IsNotNull(microsoftBookmark);
Assert.AreEqual("https://www.microsoft.com", microsoftBookmark.Bookmark);
Assert.IsNotNull(githubBookmark);
Assert.AreEqual("https://github.com", githubBookmark.Bookmark);
}
[TestMethod]
public void ValidateWebUrlDetection()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
// Assert
Assert.IsNotNull(microsoftBookmark);
Assert.IsTrue(microsoftBookmark.IsWebUrl());
}
}

View File

@ -0,0 +1,28 @@
// 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.
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
public static class Settings
{
public static Bookmarks CreateDefaultBookmarks()
{
var bookmarks = new Bookmarks();
// Add some test bookmarks
bookmarks.Data.Add(new BookmarkData
{
Name = "Microsoft",
Bookmark = "https://www.microsoft.com",
});
bookmarks.Data.Add(new BookmarkData
{
Name = "GitHub",
Bookmark = "https://github.com",
});
return bookmarks;
}
}

View File

@ -19,16 +19,23 @@ public partial class AllAppsCommandProvider : CommandProvider
public static readonly AllAppsPage Page = new();
private readonly AllAppsPage _page;
private readonly CommandItem _listItem;
public AllAppsCommandProvider()
: this(Page)
{
}
public AllAppsCommandProvider(AllAppsPage page)
{
_page = page ?? throw new ArgumentNullException(nameof(page));
Id = WellKnownId;
DisplayName = Resources.installed_apps;
Icon = Icons.AllAppsIcon;
Settings = AllAppsSettings.Instance.Settings;
_listItem = new(Page)
_listItem = new(_page)
{
Subtitle = Resources.search_installed_apps,
MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)],
@ -38,11 +45,11 @@ public partial class AllAppsCommandProvider : CommandProvider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
}
public override ICommandItem[] TopLevelCommands() => [_listItem, ..Page.GetPinnedApps()];
public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName)
{
var items = Page.GetItems();
var items = _page.GetItems();
// We're going to do this search in two directions:
// First, is this name a substring of any app...

View File

@ -2,6 +2,7 @@
// 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;
@ -19,13 +20,20 @@ namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AllAppsPage : ListPage
{
private readonly Lock _listLock = new();
private readonly IAppCache _appCache;
private AppItem[] allApps = [];
private AppListItem[] unpinnedApps = [];
private AppListItem[] pinnedApps = [];
public AllAppsPage()
: this(AppCache.Instance.Value)
{
}
public AllAppsPage(IAppCache appCache)
{
_appCache = appCache ?? throw new ArgumentNullException(nameof(appCache));
this.Name = Resources.all_apps;
this.Icon = Icons.AllAppsIcon;
this.ShowDetails = true;
@ -59,7 +67,7 @@ public sealed partial class AllAppsPage : ListPage
private void BuildListItems()
{
if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload())
if (allApps.Length == 0 || _appCache.ShouldReload())
{
lock (_listLock)
{
@ -75,7 +83,7 @@ public sealed partial class AllAppsPage : ListPage
this.IsLoading = false;
AppCache.Instance.Value.ResetReloadFlag();
_appCache.ResetReloadFlag();
stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
@ -85,11 +93,11 @@ public sealed partial class AllAppsPage : ListPage
private AppItem[] GetAllApps()
{
var uwpResults = AppCache.Instance.Value.UWPs
var uwpResults = _appCache.UWPs
.Where((application) => application.Enabled)
.Select(app => app.ToAppItem());
var win32Results = AppCache.Instance.Value.Win32s
var win32Results = _appCache.Win32s
.Where((application) => application.Enabled && application.Valid)
.Select(app => app.ToAppItem());

View File

@ -5,13 +5,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps;
public class AllAppsSettings : JsonSettingsManager
public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
{
private static readonly string _namespace = "apps";

View File

@ -12,7 +12,7 @@ using Microsoft.CmdPal.Ext.Apps.Utils;
namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AppCache : IDisposable
public sealed partial class AppCache : IAppCache, IDisposable
{
private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper;
@ -24,7 +24,7 @@ public sealed partial class AppCache : IDisposable
public IList<Win32Program> Win32s => _win32ProgramRepository.Items;
public IList<UWPApplication> UWPs => _packageRepository.Items;
public IList<IUWPApplication> UWPs => _packageRepository.Items;
public static readonly Lazy<AppCache> Instance = new(() => new());

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.Collections.Generic;
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
public interface ISettingsInterface
{
public bool EnableStartMenuSource { get; }
public bool EnableDesktopSource { get; }
public bool EnableRegistrySource { get; }
public bool EnablePathEnvironmentVariableSource { get; }
public List<string> ProgramSuffixes { get; }
public List<string> RunCommandSuffixes { get; }
}

View File

@ -0,0 +1,37 @@
// 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.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps;
/// <summary>
/// Interface for application cache that provides access to Win32 and UWP applications.
/// </summary>
public interface IAppCache : IDisposable
{
/// <summary>
/// Gets the collection of Win32 programs.
/// </summary>
IList<Win32Program> Win32s { get; }
/// <summary>
/// Gets the collection of UWP applications.
/// </summary>
IList<IUWPApplication> UWPs { get; }
/// <summary>
/// Determines whether the cache should be reloaded.
/// </summary>
/// <returns>True if cache should be reloaded, false otherwise.</returns>
bool ShouldReload();
/// <summary>
/// Resets the reload flag.
/// </summary>
void ResetReloadFlag();
}

View File

@ -0,0 +1,43 @@
// 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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
/// <summary>
/// Interface for UWP applications to enable testing and mocking
/// </summary>
public interface IUWPApplication : IProgram
{
string AppListEntry { get; set; }
string DisplayName { get; set; }
string UserModelId { get; set; }
string BackgroundColor { get; set; }
string EntryPoint { get; set; }
bool CanRunElevated { get; set; }
string LogoPath { get; set; }
LogoType LogoType { get; set; }
UWP Package { get; set; }
string LocationLocalized { get; }
string GetAppIdentifier();
List<IContextItem> GetCommands();
void UpdateLogoPath(Utils.Theme theme);
AppItem ToAppItem();
}

View File

@ -22,7 +22,7 @@ using Theme = Microsoft.CmdPal.Ext.Apps.Utils.Theme;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Serializable]
public class UWPApplication : IProgram
public class UWPApplication : IUWPApplication
{
private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path;
@ -517,7 +517,7 @@ public class UWPApplication : IProgram
}
}
internal AppItem ToAppItem()
public AppItem ToAppItem()
{
var app = this;
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;

View File

@ -0,0 +1,7 @@
// 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Apps.UnitTests")]

View File

@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Storage;
/// A repository for storing packaged applications such as UWP apps or appx packaged desktop apps.
/// This repository will also monitor for changes to the PackageCatalog and update the repository accordingly
/// </summary>
internal sealed partial class PackageRepository : ListRepository<UWPApplication>, IProgramRepository
internal sealed partial class PackageRepository : ListRepository<IUWPApplication>, IProgramRepository
{
private readonly IPackageCatalog _packageCatalog;

View File

@ -0,0 +1,45 @@
// 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 System.Text.Json;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class BookmarkJsonParser
{
public BookmarkJsonParser()
{
}
public Bookmarks ParseBookmarks(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return new Bookmarks();
}
try
{
var bookmarks = JsonSerializer.Deserialize<Bookmarks>(json, BookmarkSerializationContext.Default.Bookmarks);
return bookmarks ?? new Bookmarks();
}
catch (JsonException ex)
{
ExtensionHost.LogMessage($"parse bookmark data failed. ex: {ex.Message}");
return new Bookmarks();
}
}
public string SerializeBookmarks(Bookmarks? bookmarks)
{
if (bookmarks == null)
{
return string.Empty;
}
return JsonSerializer.Serialize(bookmarks, BookmarkSerializationContext.Default.Bookmarks);
}
}

View File

@ -11,34 +11,4 @@ namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed class Bookmarks
{
public List<BookmarkData> Data { get; set; } = [];
private static readonly JsonSerializerOptions _jsonOptions = new()
{
IncludeFields = true,
};
public static Bookmarks ReadFromFile(string path)
{
var data = new Bookmarks();
// if the file exists, load it and append the new item
if (File.Exists(path))
{
var jsonStringReading = File.ReadAllText(path);
if (!string.IsNullOrEmpty(jsonStringReading))
{
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, BookmarkSerializationContext.Default.Bookmarks) ?? new Bookmarks();
}
}
return data;
}
public static void WriteToFile(string path, Bookmarks data)
{
var jsonString = JsonSerializer.Serialize(data, BookmarkSerializationContext.Default.Bookmarks);
File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString);
}
}

View File

@ -20,10 +20,20 @@ public partial class BookmarksCommandProvider : CommandProvider
private readonly AddBookmarkPage _addNewCommand = new(null);
private readonly IBookmarkDataSource _dataSource;
private readonly BookmarkJsonParser _parser;
private Bookmarks? _bookmarks;
public BookmarksCommandProvider()
: this(new FileBookmarkDataSource(StateJsonPath()))
{
}
internal BookmarksCommandProvider(IBookmarkDataSource dataSource)
{
_dataSource = dataSource;
_parser = new BookmarkJsonParser();
Id = "Bookmarks";
DisplayName = Resources.bookmarks_display_name;
Icon = Icons.PinIcon;
@ -49,10 +59,14 @@ public partial class BookmarksCommandProvider : CommandProvider
private void SaveAndUpdateCommands()
{
if (_bookmarks is not null)
try
{
var jsonPath = BookmarksCommandProvider.StateJsonPath();
Bookmarks.WriteToFile(jsonPath, _bookmarks);
var jsonData = _parser.SerializeBookmarks(_bookmarks);
_dataSource.SaveBookmarkData(jsonData);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save bookmarks: {ex.Message}");
}
LoadCommands();
@ -82,11 +96,8 @@ public partial class BookmarksCommandProvider : CommandProvider
{
try
{
var jsonFile = StateJsonPath();
if (File.Exists(jsonFile))
{
_bookmarks = Bookmarks.ReadFromFile(jsonFile);
}
var jsonData = _dataSource.GetBookmarkData();
_bookmarks = _parser.ParseBookmarks(jsonData);
}
catch (Exception ex)
{

View File

@ -0,0 +1,49 @@
// 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.IO;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class FileBookmarkDataSource : IBookmarkDataSource
{
private readonly string _filePath;
public FileBookmarkDataSource(string filePath)
{
_filePath = filePath;
}
public string GetBookmarkData()
{
if (!File.Exists(_filePath))
{
return string.Empty;
}
try
{
return File.ReadAllText(_filePath);
}
catch (Exception ex)
{
ExtensionHost.LogMessage($"Read bookmark data failed. ex: {ex.Message}");
return string.Empty;
}
}
public void SaveBookmarkData(string jsonData)
{
try
{
File.WriteAllText(_filePath, jsonData);
}
catch (Exception ex)
{
ExtensionHost.LogMessage($"Failed to save bookmark data: {ex}");
}
}
}

View File

@ -0,0 +1,11 @@
// 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.
namespace Microsoft.CmdPal.Ext.Bookmarks;
public interface IBookmarkDataSource
{
string GetBookmarkData();
void SaveBookmarkData(string jsonData);
}

View File

@ -0,0 +1,7 @@
// 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Bookmarks.UnitTests")]

View File

@ -8,7 +8,6 @@ using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
using Microsoft.CmdPal.Ext.WindowsTerminal.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Xaml.Media.Imaging;
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Pages;
@ -16,7 +15,6 @@ internal sealed partial class ProfilesListPage : ListPage
{
private readonly TerminalQuery _terminalQuery = new();
private readonly SettingsManager _terminalSettings;
private readonly Dictionary<string, BitmapImage> _logoCache = [];
private bool showHiddenProfiles;
private bool openNewTab;
@ -54,14 +52,6 @@ internal sealed partial class ProfilesListPage : ListPage
MoreCommands = [
new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake)),
],
// Icon = () => GetLogo(profile.Terminal),
// Action = _ =>
// {
// Launch(profile.Terminal.AppUserModelId, profile.Name);
// return true;
// },
// ContextData = profile,
#pragma warning restore SA1108
});
}
@ -70,17 +60,4 @@ internal sealed partial class ProfilesListPage : ListPage
}
public override IListItem[] GetItems() => Query().ToArray();
private BitmapImage GetLogo(TerminalPackage terminal)
{
var aumid = terminal.AppUserModelId;
if (!_logoCache.TryGetValue(aumid, out var value))
{
value = terminal.GetLogo();
_logoCache.Add(aumid, value);
}
return value;
}
}

View File

@ -5,7 +5,6 @@
using System;
using System.IO;
using ManagedCommon;
using Microsoft.UI.Xaml.Media.Imaging;
// using Wox.Infrastructure.Image;
namespace Microsoft.CmdPal.Ext.WindowsTerminal;
@ -30,23 +29,4 @@ public class TerminalPackage
SettingsPath = settingsPath;
LogoPath = logoPath;
}
public BitmapImage GetLogo()
{
var image = new BitmapImage();
if (File.Exists(LogoPath))
{
using var fileStream = File.OpenRead(LogoPath);
image.SetSource(fileStream.AsRandomAccessStream());
}
else
{
// Not using wox anymore, TODO: find the right new way to handle this
// image.UriSource = new Uri(ImageLoader.ErrorIconPath);
Logger.LogError($"Logo file not found: {LogoPath}");
}
return image;
}
}

View File

@ -31,6 +31,10 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget

View File

@ -225,11 +225,6 @@ internal sealed partial class SampleContentForm : FormContent
}
]
}
},
{
"type": "Action.OpenUrl",
"title": "Action.OpenUrl",
"url": "https://adaptivecards.microsoft.com/"
}
]
}

View File

@ -34,6 +34,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

View File

@ -65,12 +65,32 @@ public partial class ListHelpers
public static void InPlaceUpdateList<T>(IList<T> original, IEnumerable<T> newContents)
where T : class
{
InPlaceUpdateList(original, newContents, out _);
}
/// <summary>
/// Modifies the contents of `original` in-place, to match those of
/// `newContents`. The canonical use being:
/// ```cs
/// ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(ItemsToFilter, TextToFilterOn));
/// ```
/// </summary>
/// <typeparam name="T">Any type that can be compared for equality</typeparam>
/// <param name="original">Collection to modify</param>
/// <param name="newContents">The enumerable which `original` should match</param>
/// <param name="removedItems">List of items that were removed from the original collection</param>
public static void InPlaceUpdateList<T>(IList<T> original, IEnumerable<T> newContents, out List<T> removedItems)
where T : class
{
removedItems = [];
// we're not changing newContents - stash this so we don't re-evaluate it every time
var numberOfNew = newContents.Count();
// Short circuit - new contents should just be empty
if (numberOfNew == 0)
{
removedItems.AddRange(original);
original.Clear();
return;
}
@ -92,6 +112,7 @@ public partial class ListHelpers
for (var k = i; k < j; k++)
{
// This item from the original list was not in the new list. Remove it.
removedItems.Add(original[i]);
original.RemoveAt(i);
}
@ -120,6 +141,7 @@ public partial class ListHelpers
while (original.Count > numberOfNew)
{
// RemoveAtEnd
removedItems.Add(original[original.Count - 1]);
original.RemoveAt(original.Count - 1);
}

View File

@ -41,8 +41,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Web.WebView2" />
<PackageReference Include="System.Drawing.Common" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
</ItemGroup>

View File

@ -180,4 +180,4 @@
<Error Condition="!Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('$(WebView2Nuget)\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(WebView2Nuget)\build\native\Microsoft.Web.WebView2.targets'))" />
</Target>
</Project>
</Project>

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4188" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.7.250513003" targetFramework="native" />
</packages>

View File

@ -45,14 +45,44 @@ public:
bool shift = false;
bool alt = false;
unsigned char key = 0;
// The id is used to identify the hotkey in the module. The order in module interface should be the same as in the settings.
int id = 0;
// Currently, this is only used by AdvancedPaste to determine if the hotkey is shown in the settings.
bool isShown = true;
std::strong_ordering operator<=>(const Hotkey&) const = default;
std::strong_ordering operator<=>(const Hotkey& other) const
{
// Compare bool fields first
if (auto cmp = (win <=> other.win); cmp != 0)
return cmp;
if (auto cmp = (ctrl <=> other.ctrl); cmp != 0)
return cmp;
if (auto cmp = (shift <=> other.shift); cmp != 0)
return cmp;
if (auto cmp = (alt <=> other.alt); cmp != 0)
return cmp;
// Compare key value only
return key <=> other.key;
// Note: Deliberately NOT comparing 'name' field
}
bool operator==(const Hotkey& other) const
{
return win == other.win &&
ctrl == other.ctrl &&
shift == other.shift &&
alt == other.alt &&
key == other.key;
}
};
struct HotkeyEx
{
WORD modifiersMask = 0;
WORD vkCode = 0;
int id = 0;
};
/* Returns the localized name of the PowerToy*/

View File

@ -66,6 +66,7 @@ const std::vector<std::pair<CLSID, CLSID>> NativeToManagedClsid({
{ CLSID_SHIMActivateMdPreviewHandler, CLSID_MdPreviewHandler },
{ CLSID_SHIMActivatePdfPreviewHandler, CLSID_PdfPreviewHandler },
{ CLSID_SHIMActivateGcodePreviewHandler, CLSID_GcodePreviewHandler },
{ CLSID_SHIMActivateBgcodePreviewHandler, CLSID_BgcodePreviewHandler },
{ CLSID_SHIMActivateQoiPreviewHandler, CLSID_QoiPreviewHandler },
{ CLSID_SHIMActivateSvgPreviewHandler, CLSID_SvgPreviewHandler },
{ CLSID_SHIMActivateSvgThumbnailProvider, CLSID_SvgThumbnailProvider }

View File

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

View File

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

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) :
handle(handle), pt_module(pt_module)
handle(handle), pt_module(pt_module), hkmng(HotkeyConflictDetector::HotkeyConflictManager::GetInstance())
{
if (!pt_module)
{
throw std::runtime_error("Module not initialized");
}
remove_hotkey_records();
update_hotkeys();
UpdateHotkeyEx();
}
@ -63,19 +64,27 @@ void PowertoyModule::update_hotkeys()
for (size_t i = 0; i < hotkeyCount; i++)
{
CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] {
Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key());
return modulePtr->on_hotkey(i);
});
if (hotkeys[i].isShown)
{
hkmng.AddHotkey(hotkeys[i], pt_module->get_key(), static_cast<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()
{
CentralizedHotkeys::UnregisterHotkeysForModule(pt_module->get_key());
auto container = pt_module->GetHotkeyEx();
if (container.has_value() && pt_module->is_enabled())
{
hkmng.RemoveHotkeyByModule(pt_module->get_key());
auto hotkey = container.value();
auto modulePtr = pt_module.get();
auto action = [modulePtr](WORD /*modifiersMask*/, WORD /*vkCode*/) {
@ -83,6 +92,9 @@ void PowertoyModule::UpdateHotkeyEx()
modulePtr->OnHotkeyEx();
};
HotkeyConflictDetector::Hotkey _hotkey = HotkeyConflictDetector::ShortcutToHotkey({ hotkey.modifiersMask, hotkey.vkCode });
hkmng.AddHotkey(_hotkey, pt_module->get_key(), 0, pt_module->is_enabled()); // This is the only one activation hotkey, so we use "0" as the name.
CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode }, { pt_module->get_key(), action });
}

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@
#include "UpdateUtils.h"
#include "centralized_kb_hook.h"
#include "Generated files/resource.h"
#include "hotkey_conflict_detector.h"
#include <common/utils/json.h>
#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())
{
moduleIt->second->set_config(settings.c_str());
moduleIt->second.remove_hotkey_records();
moduleIt->second.update_hotkeys();
moduleIt->second.UpdateHotkeyEx();
}
@ -249,6 +252,77 @@ void dispatch_received_json(const std::wstring& json_to_parse)
const std::wstring save_file_location = PTSettingsHelper::get_root_save_folder_location() + language_filename;
json::to_file(save_file_location, j);
}
else if (name == L"check_hotkey_conflict")
{
try
{
PowertoyModuleIface::Hotkey hotkey;
hotkey.win = value.GetObjectW().GetNamedBoolean(L"win", false);
hotkey.ctrl = value.GetObjectW().GetNamedBoolean(L"ctrl", false);
hotkey.shift = value.GetObjectW().GetNamedBoolean(L"shift", false);
hotkey.alt = value.GetObjectW().GetNamedBoolean(L"alt", false);
hotkey.key = static_cast<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;
}

View File

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

View File

@ -28,16 +28,23 @@ public sealed class AdvancedPasteAdditionalActions
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;
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.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Library;
@ -20,6 +20,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private bool _canMoveUp;
private bool _canMoveDown;
private bool _isValid;
private bool _hasConflict;
private string _tooltip;
[JsonPropertyName("id")]
public int Id
@ -65,7 +67,6 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
// We null-coalesce here rather than outside this branch as we want to raise PropertyChanged when the setter is called
// with null; the ShortcutControl depends on this.
_shortcut = value ?? new();
OnPropertyChanged();
}
}
@ -99,6 +100,20 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private set => Set(ref _isValid, value);
}
[JsonIgnore]
public bool HasConflict
{
get => _hasConflict;
set => Set(ref _hasConflict, value);
}
[JsonIgnore]
public string Tooltip
{
get => _tooltip;
set => Set(ref _tooltip, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [];
@ -118,6 +133,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
IsShown = other.IsShown;
CanMoveUp = other.CanMoveUp;
CanMoveDown = other.CanMoveDown;
HasConflict = other.HasConflict;
Tooltip = other.Tooltip;
}
private HotkeySettings GetShortcutClone()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig
public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "FindMyMouse";
@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
public ModuleType GetModuleType() => ModuleType.FindMyMouse;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<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.
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.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public record HotkeySettings : ICmdLineRepresentable
public record HotkeySettings : ICmdLineRepresentable, INotifyPropertyChanged
{
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()
{
@ -23,6 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = false;
Shift = false;
Code = 0;
HasConflict = false;
}
/// <summary>
@ -40,6 +54,51 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = alt;
Shift = shift;
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")]

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.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig
public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Measure Tool";
@ -25,6 +27,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string GetModuleName()
=> 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.
public bool UpgradeSettingsConfiguration()
=> false;

View File

@ -2,15 +2,17 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig
public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "MouseHighlighter";
@ -29,6 +31,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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.
public bool UpgradeSettingsConfiguration()
{

View File

@ -3,16 +3,18 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using MouseJump.Common.Helpers;
using MouseJump.Common.Models.Settings;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MouseJumpSettings : BasePTModuleSettings, ISettingsConfig
public class MouseJumpSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "MouseJump";
@ -46,6 +48,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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.
public bool UpgradeSettingsConfiguration()
{

View File

@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MousePointerCrosshairsSettings : BasePTModuleSettings, ISettingsConfig
public class MousePointerCrosshairsSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "MousePointerCrosshairs";
@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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.
public bool UpgradeSettingsConfiguration()
{

View File

@ -3,15 +3,17 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MouseWithoutBordersSettings : BasePTModuleSettings, ISettingsConfig
public class MouseWithoutBordersSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "MouseWithoutBorders";
@ -37,6 +39,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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)
{
// VK_A <= value <= VK_Z

View File

@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class PeekSettings : BasePTModuleSettings, ISettingsConfig
public class PeekSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Peek";
public const string ModuleVersion = "0.0.1";
@ -35,6 +37,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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()
{
return false;

View File

@ -6,12 +6,13 @@ using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class PowerLauncherSettings : BasePTModuleSettings, ISettingsConfig
public class PowerLauncherSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "PowerToys Run";
@ -49,6 +50,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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.
public bool UpgradeSettingsConfiguration()
{

View File

@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class PowerOcrSettings : BasePTModuleSettings, ISettingsConfig
public class PowerOcrSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "TextExtractor";
@ -42,6 +44,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string GetModuleName()
=> 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.
public bool UpgradeSettingsConfiguration()
=> 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.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class ShortcutGuideSettings : BasePTModuleSettings, ISettingsConfig
public class ShortcutGuideSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Shortcut Guide";
@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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.
public bool UpgradeSettingsConfiguration()
{

View File

@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class WorkspacesSettings : BasePTModuleSettings, ISettingsConfig
public class WorkspacesSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Workspaces";
public const string ModuleVersion = "0.0.1";
@ -39,6 +41,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
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)
{
// Save settings to file

View File

@ -3,8 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
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.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -13,7 +13,7 @@ using Moq;
namespace ViewModelTests
{
[TestClass]
public class PowerLauncherViewModelTest
public class PowerLauncherViewModelTest : IDisposable
{
private sealed class SendCallbackMock
{
@ -26,20 +26,48 @@ namespace ViewModelTests
{
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 PowerLauncherSettings mockSettings;
private SendCallbackMock sendCallbackMock;
private BackCompatTestProperties.MockSettingsRepository<GeneralSettings> mockGeneralSettingsRepository;
[TestInitialize]
public void Initialize()
{
mockSettings = new PowerLauncherSettings();
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(
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>
@ -67,7 +95,7 @@ namespace ViewModelTests
// Initialise View Model with test Config files
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
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;
[JsonSerializable(typeof(WINDOWPLACEMENT))]
[JsonSerializable(typeof(ActionMessage))]
[JsonSerializable(typeof(AdvancedPasteSettings))]
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
[JsonSerializable(typeof(AlwaysOnTopSettings))]
[JsonSerializable(typeof(ColorPickerSettings))]
[JsonSerializable(typeof(CropAndLockSettings))]
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
[JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
[JsonSerializable(typeof(MouseJumpSettings))]
[JsonSerializable(typeof(MousePointerCrosshairsSettings))]
[JsonSerializable(typeof(MouseWithoutBordersSettings))]
[JsonSerializable(typeof(NewPlusSettings))]
[JsonSerializable(typeof(PeekSettings))]
[JsonSerializable(typeof(PowerLauncherSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(RegistryPreviewSettings))]
[JsonSerializable(typeof(ShortcutGuideSettings))]
[JsonSerializable(typeof(WINDOWPLACEMENT))]
[JsonSerializable(typeof(WorkspacesSettings))]
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(ActionMessage))]
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();
GlobalHotkeyConflictManager.Initialize(message =>
{
ipcmanager.Send(message);
return 0;
});
if (!ShowOobe && !ShowScoobe && !ShowFlyout)
{
settingsWindow = new MainWindow();
@ -320,10 +326,18 @@ namespace Microsoft.PowerToys.Settings.UI
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
settingsWindow.Activate();
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
/* 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);
Exit();
/* 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);
Exit();
#endif
}
}

View File

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

View File

@ -4,37 +4,122 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.ComponentModel;
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.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
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;
using Microsoft.Windows.ApplicationModel.Resources;
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()
{
InitializeComponent();
GetShortcutConflicts();
}
DataContext = this;
private void GetShortcutConflicts()
{
// TO DO: Implement the logic to retrieve and display shortcut conflicts. Make sure to Collapse this control if not conflicts are found.
// Initially hide the control if no conflicts
Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed;
}
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>

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