mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-30 17:07:23 +01:00
Compare commits
35 Commits
jay/DarkMo
...
leilzh/tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06c21166b9 | ||
|
|
40bfdbc7c0 | ||
|
|
12c84b19c9 | ||
|
|
759f5c02cb | ||
|
|
e0428eef1d | ||
|
|
3bc746d0ff | ||
|
|
75526b9580 | ||
|
|
ce4d8dc11e | ||
|
|
917da2e07e | ||
|
|
501ff5690d | ||
|
|
38acf1a6bc | ||
|
|
cb70e1dbfd | ||
|
|
df750394db | ||
|
|
9bcba8cb8b | ||
|
|
00457b2933 | ||
|
|
2b0b80c63a | ||
|
|
d42df8392c | ||
|
|
c6033bc8e5 | ||
|
|
69c206e85e | ||
|
|
0afb91b71d | ||
|
|
f926a5ece9 | ||
|
|
b9e66cca26 | ||
|
|
c4dd213712 | ||
|
|
2808f65d7f | ||
|
|
0e61b28571 | ||
|
|
ce406892b3 | ||
|
|
f619334ff2 | ||
|
|
142f09459d | ||
|
|
f42115bc4f | ||
|
|
36764487c5 | ||
|
|
218cb473c1 | ||
|
|
f0b674763a | ||
|
|
cf0264e9b9 | ||
|
|
be7098c267 | ||
|
|
3f486ea3db |
34
.github/actions/spell-check/expect.txt
vendored
34
.github/actions/spell-check/expect.txt
vendored
@@ -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
|
||||
|
||||
@@ -147,10 +147,10 @@ jobs:
|
||||
& "$(build.sourcesdirectory)\.pipelines\installPowerToys.ps1" -InstallMode "Machine"
|
||||
displayName: Install PowerToys (Machine-Level)
|
||||
|
||||
- ${{ if ne(parameters.platform, 'arm64') }}:
|
||||
- task: ScreenResolutionUtility@1
|
||||
inputs:
|
||||
displaySettings: 'optimal'
|
||||
# - ${{ if ne(parameters.platform, 'arm64') }}:
|
||||
# - task: ScreenResolutionUtility@1
|
||||
# inputs:
|
||||
# displaySettings: 'optimal'
|
||||
|
||||
- ${{ if eq(length(parameters.uiTestModules), 0) }}:
|
||||
- task: VSTest@3
|
||||
|
||||
@@ -788,6 +788,14 @@ 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1-76DF-42AC-985C-164CC2EE57A1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
|
||||
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 +2858,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 +3185,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}
|
||||
|
||||
@@ -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 module’s settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`.
|
||||
|
||||
- This method should return all hotkeys used in the module.
|
||||
- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`).
|
||||
- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs`
|
||||
- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings.
|
||||
It provides both `getter` and `setter` methods to read and update the corresponding hotkey.
|
||||
Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey.
|
||||
This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`
|
||||
|
||||
### 3. Update the module’s ViewModel
|
||||
The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary<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 module’s Views call `OnPageLoaded()`
|
||||
Once the module’s view is loaded, make sure to invoke the ViewModel’s `OnPageLoaded()` method:
|
||||
```cs
|
||||
Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||
```
|
||||
- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
|
||||
|
||||
## Debugging Settings
|
||||
|
||||
To debug settings issues:
|
||||
|
||||
@@ -18,8 +18,8 @@ You can build the entire solution from the command line, which is sometimes fast
|
||||
1. Open Developer Command Prompt for VS 2022
|
||||
2. Navigate to the repository root directory
|
||||
3. Run the following command(don't forget to set the correct platform):
|
||||
```
|
||||
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln
|
||||
```pwsh
|
||||
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln /tl /p:NuGetInteractive="true"
|
||||
```
|
||||
4. This process should complete in approximately 13-14 minutes for a full build
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
Workspaces,
|
||||
PowerRename,
|
||||
CommandPalette,
|
||||
ScreenRuler,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -104,6 +105,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
[PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor"),
|
||||
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
|
||||
[PowerToysModule.CommandPalette] = new ModuleInfo("Microsoft.CmdPal.UI.exe", "PowerToys Command Palette", "WinUI3Apps\\CmdPal"),
|
||||
[PowerToysModule.ScreenRuler] = new ModuleInfo("PowerToys.MeasureToolUI.exe", "PowerToys.ScreenRuler", "WinUI3Apps"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,11 @@ struct CommonState
|
||||
|
||||
Measurement::Unit units = Measurement::Unit::Pixel;
|
||||
|
||||
POINT cursorPosSystemSpace = {}; // updated atomically
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4324)
|
||||
alignas(8) POINT cursorPosSystemSpace = {}; // updated atomically
|
||||
#pragma warning(pop)
|
||||
|
||||
std::atomic_bool closeOnOtherMonitors = false;
|
||||
|
||||
float GetPhysicalPx2MmRatio(HWND window) const
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="PowerToys.ScreenRuler"
|
||||
IsAlwaysOnTop="True"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
@@ -250,6 +251,7 @@
|
||||
<ToggleButton
|
||||
Name="btnBounds"
|
||||
x:Uid="BtnBounds"
|
||||
AutomationProperties.AutomationId="Button_Bounds"
|
||||
Click="BoundsTool_Click"
|
||||
Content=""
|
||||
KeyboardAcceleratorPlacementMode="Auto"
|
||||
@@ -267,6 +269,7 @@
|
||||
<ToggleButton
|
||||
Name="btnSpacing"
|
||||
x:Uid="BtnSpacing"
|
||||
AutomationProperties.AutomationId="Button_Spacing"
|
||||
Click="MeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -284,6 +287,7 @@
|
||||
<ToggleButton
|
||||
Name="btnHorizontalSpacing"
|
||||
x:Uid="BtnHorizontalSpacing"
|
||||
AutomationProperties.AutomationId="Button_SpacingHorizontal"
|
||||
Click="HorizontalMeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -304,6 +308,7 @@
|
||||
<ToggleButton
|
||||
Name="btnVerticalSpacing"
|
||||
x:Uid="BtnVerticalSpacing"
|
||||
AutomationProperties.AutomationId="Button_SpacingVertical"
|
||||
Click="VerticalMeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -324,6 +329,7 @@
|
||||
<AppBarSeparator />
|
||||
<Button
|
||||
x:Uid="BtnClosePanel"
|
||||
AutomationProperties.AutomationId="Button_Close"
|
||||
Click="ClosePanelTool_Click"
|
||||
Content=""
|
||||
Foreground="{StaticResource CloseButtonBackgroundPointerOver}">
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="d4d0f157-5c12-4390-9689-152b0c86a582"
|
||||
Publisher="CN=gkhmyznikov"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="d4d0f157-5c12-4390-9689-152b0c86a582" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>ScreenRuler.UITests</DisplayName>
|
||||
<PublisherDisplayName>gkhmyznikov</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="ScreenRuler.UITests"
|
||||
Description="ScreenRuler.UITests"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>PowerToys.ScreenRuler.UITests</RootNamespace>
|
||||
<AssemblyName>ScreenRuler.UITests</AssemblyName>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
|
||||
<!-- This is a UI test, so don't run as part of MSBuild -->
|
||||
<RunVSTest>false</RunVSTest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\ScreenRuler.UITests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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 Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestBounds : UITestBase
|
||||
{
|
||||
public TestBounds()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.BoundsTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerBoundsTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "bounds test");
|
||||
TestHelper.PerformBoundsToolTest(this);
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
466
src/modules/MeasureTool/Tests/ScreenRuler.UITests/TestHelper.cs
Normal file
466
src/modules/MeasureTool/Tests/ScreenRuler.UITests/TestHelper.cs
Normal file
@@ -0,0 +1,466 @@
|
||||
// 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.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
public static class TestHelper
|
||||
{
|
||||
private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
|
||||
|
||||
// Button automation names from Resources.resw
|
||||
public const string BoundsButtonId = "Button_Bounds";
|
||||
public const string SpacingButtonName = "Button_Spacing";
|
||||
public const string HorizontalSpacingButtonName = "Button_SpacingHorizontal";
|
||||
public const string VerticalSpacingButtonName = "Button_SpacingVertical";
|
||||
public const string CloseButtonId = "Button_Close";
|
||||
|
||||
/// <summary>
|
||||
/// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
/// <param name="testName">Name of the test for assertions</param>
|
||||
/// <returns>The activation keys for the test</returns>
|
||||
public static Key[] InitializeTest(UITestBase testBase, string testName)
|
||||
{
|
||||
LaunchFromSetting(testBase);
|
||||
|
||||
var toggleSwitch = SetScreenRulerToggle(testBase, enable: true);
|
||||
Assert.IsTrue(
|
||||
toggleSwitch.IsOn,
|
||||
$"Screen Ruler toggle switch should be ON for {testName}");
|
||||
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
Assert.IsNotNull(activationKeys, "Should be able to read activation shortcut");
|
||||
Assert.IsTrue(activationKeys.Length > 0, "Activation shortcut should contain at least one key");
|
||||
|
||||
return activationKeys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs common test cleanup: close ScreenRuler UI
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
public static void CleanupTest(UITestBase testBase)
|
||||
{
|
||||
CloseScreenRulerUI(testBase);
|
||||
|
||||
// Ensure we're attached to settings after cleanup
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors - this is just cleanup
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the Screen Ruler (Measure Tool) settings page
|
||||
/// </summary>
|
||||
public static void LaunchFromSetting(UITestBase testBase)
|
||||
{
|
||||
var screenRulers = testBase.Session.FindAll<NavigationViewItem>(By.AccessibilityId("Shell_Nav_ScreenRuler"));
|
||||
|
||||
if (screenRulers.Count == 0)
|
||||
{
|
||||
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("Shell_Nav_TopLevelSystemTools"), 5000).Click(msPostAction: 500);
|
||||
}
|
||||
|
||||
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("Shell_Nav_ScreenRuler"), 5000).Click(msPostAction: 500);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Screen Ruler toggle switch to the specified state
|
||||
/// </summary>
|
||||
public static ToggleSwitch SetScreenRulerToggle(UITestBase testBase, bool enable)
|
||||
{
|
||||
var toggleSwitch = testBase.Session.Find<ToggleSwitch>(By.AccessibilityId("Toggle_ScreenRuler"), 5000);
|
||||
|
||||
if (toggleSwitch.IsOn != enable)
|
||||
{
|
||||
toggleSwitch.Click(msPreAction: 1000, msPostAction: 2000);
|
||||
}
|
||||
|
||||
if (toggleSwitch.IsOn != enable)
|
||||
{
|
||||
testBase.Session.SendKey(Key.Space, msPreAction: 0, msPostAction: 2000);
|
||||
}
|
||||
|
||||
return toggleSwitch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Screen Ruler toggle and verify its state
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
/// <param name="enable">True to enable, false to disable</param>
|
||||
/// <param name="testName">Name of the test for assertion messages</param>
|
||||
public static void SetAndVerifyScreenRulerToggle(UITestBase testBase, bool enable, string testName)
|
||||
{
|
||||
var toggleSwitch = SetScreenRulerToggle(testBase, enable);
|
||||
Assert.AreEqual(
|
||||
enable,
|
||||
toggleSwitch.IsOn,
|
||||
$"Screen Ruler toggle switch should be {(enable ? "ON" : "OFF")} for {testName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the current activation shortcut from the ShortcutControl
|
||||
/// </summary>
|
||||
public static Key[] ReadActivationShortcut(UITestBase testBase)
|
||||
{
|
||||
var shortcutCard = testBase.Session.Find<Element>(By.AccessibilityId("Shortcut_ScreenRuler"), 5000);
|
||||
var shortcutButton = shortcutCard.Find<Element>(By.AccessibilityId("EditButton"), 5000);
|
||||
return ParseShortcutText(shortcutButton.HelpText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse shortcut text like "Win + Ctrl + Shift + M" into Key array
|
||||
/// </summary>
|
||||
private static Key[] ParseShortcutText(string shortcutText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(shortcutText))
|
||||
{
|
||||
return new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.M };
|
||||
}
|
||||
|
||||
var keys = new List<Key>();
|
||||
var parts = shortcutText.Split(ShortcutSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var cleanPart = part.Trim().ToLowerInvariant();
|
||||
var key = cleanPart switch
|
||||
{
|
||||
"win" or "windows" => Key.Win,
|
||||
"ctrl" or "control" => Key.Ctrl,
|
||||
"shift" => Key.Shift,
|
||||
"alt" => Key.Alt,
|
||||
_ when cleanPart.Length == 1 && char.IsLetter(cleanPart[0]) &&
|
||||
cleanPart[0] >= 'a' && cleanPart[0] <= 'z' =>
|
||||
(Key)Enum.Parse(typeof(Key), cleanPart.ToUpperInvariant()),
|
||||
_ => (Key?)null,
|
||||
};
|
||||
|
||||
if (key.HasValue)
|
||||
{
|
||||
keys.Add(key.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return keys.Count > 0 ? keys.ToArray() : new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.M };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if ScreenRulerUI window is open
|
||||
/// </summary>
|
||||
public static bool IsScreenRulerUIOpen(UITestBase testBase) => testBase.IsWindowOpen("PowerToys.ScreenRuler");
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to reach the specified state within the timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUIState(UITestBase testBase, bool shouldBeOpen, int timeoutMs = 5000, int pollingIntervalMs = 100)
|
||||
{
|
||||
var endTime = DateTime.Now.AddMilliseconds(timeoutMs);
|
||||
|
||||
while (DateTime.Now < endTime)
|
||||
{
|
||||
if (IsScreenRulerUIOpen(testBase) == shouldBeOpen)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Task.Delay(pollingIntervalMs).Wait();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to appear within the specified timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUI(UITestBase testBase, int timeoutMs = 5000) =>
|
||||
WaitForScreenRulerUIState(testBase, shouldBeOpen: true, timeoutMs);
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to disappear within the specified timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUIToDisappear(UITestBase testBase, int timeoutMs = 5000) =>
|
||||
WaitForScreenRulerUIState(testBase, shouldBeOpen: false, timeoutMs);
|
||||
|
||||
/// <summary>
|
||||
/// Close ScreenRulerUI if it's open
|
||||
/// </summary>
|
||||
public static void CloseScreenRulerUI(UITestBase testBase)
|
||||
{
|
||||
if (IsScreenRulerUIOpen(testBase))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Attach to ScreenRuler window before trying to find and click close button
|
||||
testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var closeButton = testBase.Session.Find<Element>(By.AccessibilityId(CloseButtonId), 15000, true);
|
||||
closeButton?.Click();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't find the close button, ignore - the window might have closed already
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Attach back to settings after closing
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific ScreenRulerUI button by its automation name
|
||||
/// </summary>
|
||||
public static Element? GetScreenRulerButton(UITestBase testBase, string buttonName, int timeoutMs = 1000)
|
||||
{
|
||||
return testBase.Session.Find<Element>(By.AccessibilityId(buttonName), timeoutMs, true);
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
// Attach to ScreenRuler window before trying to find buttons
|
||||
testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
return testBase.Session.Find<Element>(By.AccessibilityId(buttonName), timeoutMs, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Attach back to settings if needed for further operations
|
||||
// This ensures we don't break the test flow
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors - the calling code will handle as needed
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the clipboard content using STA thread
|
||||
/// </summary>
|
||||
public static void ClearClipboard()
|
||||
{
|
||||
ExecuteInSTAThread(() => System.Windows.Forms.Clipboard.Clear());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get text content from clipboard using STA thread
|
||||
/// </summary>
|
||||
public static string GetClipboardText()
|
||||
{
|
||||
string result = string.Empty;
|
||||
ExecuteInSTAThread(() =>
|
||||
{
|
||||
if (System.Windows.Forms.Clipboard.ContainsText())
|
||||
{
|
||||
result = System.Windows.Forms.Clipboard.GetText();
|
||||
}
|
||||
});
|
||||
return result ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute an action in an STA thread with error handling
|
||||
/// </summary>
|
||||
private static void ExecuteInSTAThread(Action action)
|
||||
{
|
||||
try
|
||||
{
|
||||
var staThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore clipboard errors
|
||||
}
|
||||
});
|
||||
|
||||
staThread.SetApartmentState(ApartmentState.STA);
|
||||
staThread.Start();
|
||||
staThread.Join(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore clipboard errors
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate clipboard content contains valid spacing measurement for the specified type
|
||||
/// </summary>
|
||||
public static bool ValidateSpacingClipboardContent(string clipboardText, string spacingType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipboardText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return spacingType switch
|
||||
{
|
||||
"Spacing" => Regex.IsMatch(clipboardText, @"\d+\s*[<5B>x]\s*\d+"),
|
||||
"Horizontal Spacing" or "Vertical Spacing" => Regex.IsMatch(clipboardText, @"^\d+$"),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a complete spacing tool test operation
|
||||
/// </summary>
|
||||
public static void PerformSpacingToolTest(UITestBase testBase, string buttonId, string testName)
|
||||
{
|
||||
ClearClipboard();
|
||||
|
||||
// Launch ScreenRuler UI
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
testBase.SendKeys(activationKeys);
|
||||
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUI(testBase, 2000),
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut for {testName}: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Attach to ScreenRuler window and click spacing button
|
||||
// testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var spacingButton = testBase.Session.Find<Element>(By.AccessibilityId(buttonId), 15000, true);
|
||||
Assert.IsNotNull(spacingButton, $"{testName} button should be found");
|
||||
|
||||
spacingButton!.Click();
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Perform measurement action (stay attached to ScreenRuler for this)
|
||||
PerformMeasurementAction(testBase);
|
||||
|
||||
// Validate results
|
||||
ValidateClipboardResults(testName);
|
||||
|
||||
// Cleanup - this will handle session attachment properly
|
||||
CloseScreenRulerUI(testBase);
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUIToDisappear(testBase, 2000),
|
||||
$"{testName}: ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a bounds tool test operation
|
||||
/// </summary>
|
||||
public static void PerformBoundsToolTest(UITestBase testBase)
|
||||
{
|
||||
ClearClipboard();
|
||||
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
testBase.SendKeys(activationKeys);
|
||||
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUI(testBase, 2000),
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Attach to ScreenRuler window and click bounds button
|
||||
// testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var boundsButton = testBase.Session.Find<Element>(By.AccessibilityId(BoundsButtonId), 15000, true);
|
||||
Assert.IsNotNull(boundsButton, "Bounds button should be found");
|
||||
|
||||
boundsButton.Click();
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Perform drag operation to create 100x100 box (stay attached to ScreenRuler)
|
||||
var currentPos = testBase.GetMousePosition();
|
||||
int startX = currentPos.Item1;
|
||||
int startY = currentPos.Item2 + 200;
|
||||
|
||||
testBase.MoveMouseTo(startX, startY);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
// Drag operation
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftDown);
|
||||
Task.Delay(100).Wait();
|
||||
|
||||
testBase.MoveMouseTo(startX + 99, startY + 99);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftUp);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Dismiss selection
|
||||
testBase.Session.PerformMouseAction(MouseActionType.RightClick);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Validate results
|
||||
string clipboardText = GetClipboardText();
|
||||
Assert.IsFalse(string.IsNullOrEmpty(clipboardText), "Clipboard should contain measurement data");
|
||||
Assert.IsTrue(
|
||||
clipboardText.Contains("100 <20> 100"),
|
||||
$"Clipboard should contain '100 <20> 100', but contained: '{clipboardText}'");
|
||||
|
||||
// Cleanup - this will handle session attachment properly
|
||||
CloseScreenRulerUI(testBase);
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUIToDisappear(testBase, 2000),
|
||||
"ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a measurement action (move mouse and click)
|
||||
/// </summary>
|
||||
private static void PerformMeasurementAction(UITestBase testBase)
|
||||
{
|
||||
var currentPos = testBase.GetMousePosition();
|
||||
int startX = currentPos.Item1;
|
||||
int startY = currentPos.Item2 + 200;
|
||||
|
||||
testBase.MoveMouseTo(startX, startY);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftClick);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.RightClick);
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate clipboard results for spacing tests
|
||||
/// </summary>
|
||||
private static void ValidateClipboardResults(string testName)
|
||||
{
|
||||
string clipboardText = GetClipboardText();
|
||||
Assert.IsFalse(string.IsNullOrEmpty(clipboardText), $"{testName}: Clipboard should contain measurement data");
|
||||
|
||||
bool containsValidPattern = ValidateSpacingClipboardContent(clipboardText, testName);
|
||||
Assert.IsTrue(
|
||||
containsValidPattern,
|
||||
$"{testName}: Clipboard should contain valid spacing measurement, but contained: '{clipboardText}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestShortcutActivation : UITestBase
|
||||
{
|
||||
public TestShortcutActivation()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.ShortcutActivation")]
|
||||
[TestCategory("Activation")]
|
||||
public void TestScreenRulerShortcutActivation()
|
||||
{
|
||||
var activationKeys = TestHelper.InitializeTest(this, "activation test");
|
||||
|
||||
// Test 1: Press the activation shortcut and verify the toolbar appears
|
||||
SendKeys(activationKeys);
|
||||
bool screenRulerAppeared = TestHelper.WaitForScreenRulerUI(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerAppeared,
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 2: Press the activation shortcut again and verify the toolbar disappears
|
||||
SendKeys(activationKeys);
|
||||
bool screenRulerDisappeared = TestHelper.WaitForScreenRulerUIToDisappear(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerDisappeared,
|
||||
$"ScreenRulerUI should disappear after pressing activation shortcut again: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 3: Disable Screen Ruler and verify that the activation shortcut no longer activates the utility
|
||||
// Ensure we're attached to settings UI before toggling
|
||||
Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
TestHelper.SetAndVerifyScreenRulerToggle(this, enable: false, "disabled state test");
|
||||
|
||||
// Try to activate with shortcut while disabled
|
||||
SendKeys(activationKeys);
|
||||
Task.Delay(1000).Wait();
|
||||
Assert.IsFalse(
|
||||
TestHelper.IsScreenRulerUIOpen(this),
|
||||
"ScreenRulerUI should not appear when Screen Ruler is disabled");
|
||||
|
||||
// Test 4: Enable Screen Ruler and press the activation shortcut and verify the toolbar appears
|
||||
// Ensure we're attached to settings UI before toggling
|
||||
Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
TestHelper.SetAndVerifyScreenRulerToggle(this, enable: true, "re-enabled state test");
|
||||
|
||||
SendKeys(activationKeys);
|
||||
screenRulerAppeared = TestHelper.WaitForScreenRulerUI(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerAppeared,
|
||||
$"ScreenRulerUI should appear after re-enabling and pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 5: Verify the utility can be closed via the cleanup method
|
||||
TestHelper.CloseScreenRulerUI(this);
|
||||
bool screenRulerClosed = TestHelper.WaitForScreenRulerUIToDisappear(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerClosed,
|
||||
"ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacing : UITestBase
|
||||
{
|
||||
public TestSpacing()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.SpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.SpacingButtonName, "Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacingHorizontal : UITestBase
|
||||
{
|
||||
public TestSpacingHorizontal()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.HorizontalSpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerHorizontalSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "horizontal spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.HorizontalSpacingButtonName, "Horizontal Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacingVertical : UITestBase
|
||||
{
|
||||
public TestSpacingVertical()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.VerticalSpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerVerticalSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "vertical spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.VerticalSpacingButtonName, "Vertical Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="ScreenRuler.UITests.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 commands:Add + 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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...
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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")]
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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")]
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -225,11 +225,6 @@ internal sealed partial class SampleContentForm : FormContent
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Action.OpenUrl",
|
||||
"title": "Action.OpenUrl",
|
||||
"url": "https://adaptivecards.microsoft.com/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
471
src/runner/hotkey_conflict_detector.cpp
Normal file
471
src/runner/hotkey_conflict_detector.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
100
src/runner/hotkey_conflict_detector.h
Normal file
100
src/runner/hotkey_conflict_detector.h
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 => [];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
197
src/settings-ui/Settings.UI.Library/SettingsFactory.cs
Normal file
197
src/settings-ui/Settings.UI.Library/SettingsFactory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user