Compare commits

..

4 Commits

Author SHA1 Message Date
Jessica Dene Earley-Cha
c1a3aad9b8 use is not, moved navigation pane open/close strings to resources 2025-08-21 13:41:33 -07:00
Jessica Dene Earley-Cha
17567d571a Merge branch 'main' into hamburger-notification 2025-08-19 13:53:33 -07:00
Jessica Dene Earley-Cha
7576429716 move Automation Notification funtionality to UIHelper impliment UIHelper in ListPage and SettingsWindow 2025-08-06 14:35:03 -07:00
Jessica Dene Earley-Cha
3dc1cea6eb remove unneeded PaneToggleBtn, Automation Notification when navigationView is open or closed 2025-08-06 10:58:08 -07:00
182 changed files with 845 additions and 7596 deletions

View File

@@ -25,14 +25,11 @@ ADMINS
adml
admx
advancedpaste
advancedpasteui
advancedpasteuishortcut
advfirewall
AFeature
affordances
AFX
AGGREGATABLE
AHK
AHybrid
akv
ALarger
@@ -43,7 +40,6 @@ ALLINPUT
Allman
Allmodule
ALLOWUNDO
allpc
ALLVIEW
ALPHATYPE
AModifier
@@ -633,7 +629,6 @@ HKCU
hkey
HKLM
HKM
hkmng
HKPD
HKU
HMD
@@ -651,11 +646,7 @@ Hostx
hotfixes
hotkeycontrol
HOTKEYF
hotkeylockmachine
hotkeyreconnect
hotkeys
hotkeyswitch
hotkeytoggleeasymouse
hotlight
hotspot
HPAINTBUFFER
@@ -668,7 +659,6 @@ HROW
hsb
HSCROLL
hsi
HSpeed
HTCLIENT
hthumbnail
HTOUCHINPUT
@@ -714,12 +704,9 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
imagetotext
imagetotextshortcut
imagingdevices
ime
imgflip
inapp
inbox
INCONTACT
Indo
@@ -773,7 +760,6 @@ istep
ith
ITHUMBNAIL
IUI
IUWP
IWIC
jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
@@ -803,7 +789,6 @@ keyvault
KILLFOCUS
killrunner
kmph
kvp
Kybd
lastcodeanalysissucceeded
LASTEXITCODE
@@ -842,7 +827,6 @@ localappdata
localpackage
LOCALSYSTEM
LOCATIONCHANGE
LOCKMACHINE
LOCKTYPE
LOGFONT
LOGFONTW
@@ -928,7 +912,6 @@ MDL
mdtext
mdtxt
mdwn
measuretool
meme
memicmp
MENUITEMINFO
@@ -978,7 +961,6 @@ MOUSEHWHEEL
MOUSEINPUT
mousejump
mousepointer
mousepointercrosshairs
mouseutils
MOVESIZEEND
MOVESIZESTART
@@ -1118,7 +1100,6 @@ NOTSRCCOPY
NOTSRCERASE
notwindows
NOTXORPEN
nowarn
NOZORDER
NPH
npmjs
@@ -1180,18 +1161,6 @@ PARENTRELATIVEFORADDRESSBAR
PARENTRELATIVEPARSING
parray
PARTIALCONFIRMATIONDIALOGTITLE
pasteashtmlfile
pasteashtmlfileshortcut
pasteasjson
pasteasjsonshortcut
pasteasmarkdown
pasteasmarkdownshortcut
pasteasplaintext
pasteasplaintextshortcut
pasteaspngfile
pasteaspngfileshortcut
pasteastxtfile
pasteastxtfileshortcut
PATCOPY
PATHMUSTEXIST
PATINVERT
@@ -1259,7 +1228,6 @@ Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
powerocr
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1400,7 +1368,6 @@ Removelnk
renamable
RENAMEONCOLLISION
reparented
reparenthotkey
reparenting
reportfileaccesses
requery
@@ -1650,7 +1617,6 @@ STYLECHANGED
STYLECHANGING
subkeys
sublang
Subdomain
SUBMODULEUPDATE
subresource
Superbar
@@ -1721,7 +1687,6 @@ THH
THICKFRAME
THISCOMPONENT
throughs
thumbnailhotkey
TILEDWINDOW
TILLSON
timedate
@@ -1736,7 +1701,6 @@ tlb
tlbimp
tlc
TNP
TOGGLEEASYMOUSE
Toolhelp
toolkitconverters
toolwindow
@@ -1750,7 +1714,6 @@ tracelogging
tracerpt
trackbar
trafficmanager
transcodetomp
transicc
TRAYMOUSEMESSAGE
triaging
@@ -1865,7 +1828,6 @@ VSINSTALLDIR
VSM
vso
vsonline
VSpeed
vstemplate
vstest
VSTHRD
@@ -2002,13 +1964,10 @@ XNamespace
Xoshiro
XPels
XPixel
XPos
XResource
xsi
XSpeed
XStr
xstyler
XTimer
XUP
XVIRTUALSCREEN
xxxxxx
@@ -2018,10 +1977,7 @@ YIncrement
yinle
yinyue
YPels
YPos
YResolution
YSpeed
YTimer
YStr
YVIRTUALSCREEN
ZEROINIT

View File

@@ -57,19 +57,12 @@ $totalList = $projFiles | ForEach-Object -Parallel {
$p = -split $p
$p = $p[1, 2]
$tempString = $p[0]
$tempString = $p[0] + " " + $p[1]
if([string]::IsNullOrWhiteSpace($tempString))
if(![string]::IsNullOrWhiteSpace($tempString))
{
Continue
echo "- $tempString";
}
if($tempString.StartsWith("Microsoft.") -Or $tempString.StartsWith("System."))
{
Continue
}
echo "- $tempString"
}
$csproj = $null;
}

View File

@@ -21,13 +21,4 @@ if (-not $?)
exit 1
}
# Ignore NU1503 on vcxproj files
dotnet restore $solution /nowarn:NU1503
if ($lastExitCode -ne 0)
{
$result = $lastExitCode
Write-Error "Error running dotnet restore, with the exit code $lastExitCode. Please verify logs on the nuget package versions."
exit $result
}
exit 0

View File

@@ -65,8 +65,7 @@
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="MSTest" Version="3.8.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="NLog" Version="5.0.4" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
<PackageVersion Include="OpenAI" Version="2.0.0" />

137
NOTICE.md
View File

@@ -1491,50 +1491,93 @@ SOFTWARE.
## NuGet Packages used by PowerToys
- AdaptiveCards.ObjectModel.WinUI3
- AdaptiveCards.Rendering.WinUI3
- AdaptiveCards.Templating
- Appium.WebDriver
- Azure.AI.OpenAI
- CoenM.ImageSharp.ImageHash
- CommunityToolkit.Common
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
- CommunityToolkit.Mvvm
- CommunityToolkit.WinUI.Animations
- CommunityToolkit.WinUI.Collections
- CommunityToolkit.WinUI.Controls.Primitives
- CommunityToolkit.WinUI.Controls.Segmented
- CommunityToolkit.WinUI.Controls.SettingsControls
- CommunityToolkit.WinUI.Controls.Sizers
- CommunityToolkit.WinUI.Converters
- CommunityToolkit.WinUI.Extensions
- CommunityToolkit.WinUI.UI.Controls.DataGrid
- CommunityToolkit.WinUI.UI.Controls.Markdown
- ControlzEx
- HelixToolkit
- HelixToolkit.Core.Wpf
- hyjiacan.pinyin4net
- Interop.Microsoft.Office.Interop.OneNote
- LazyCache
- Mages
- Markdig.Signed
- MessagePack
- ModernWpfUI
- Moq
- MSTest
- NLog
- NLog.Extensions.Logging
- NLog.Schema
- OpenAI
- ReverseMarkdown
- ScipBe.Common.Office.OneNote
- SharpCompress
- SkiaSharp.Views.WinUI
- StreamJsonRpc
- StyleCop.Analyzers
- UnicodeInformation
- UnitsNet
- UTF.Unknown
- WinUIEx
- WPF-UI
- WyHash
- AdaptiveCards.ObjectModel.WinUI3 2.0.0-beta
- AdaptiveCards.Rendering.WinUI3 2.1.0-beta
- AdaptiveCards.Templating 2.0.5
- Appium.WebDriver 4.4.5
- Azure.AI.OpenAI 1.0.0-beta.17
- CoenM.ImageSharp.ImageHash 1.3.6
- CommunityToolkit.Common 8.4.0
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock 0.1.250703-build.2173
- CommunityToolkit.Mvvm 8.4.0
- CommunityToolkit.WinUI.Animations 8.2.250402
- CommunityToolkit.WinUI.Collections 8.2.250402
- CommunityToolkit.WinUI.Controls.Primitives 8.2.250402
- CommunityToolkit.WinUI.Controls.Segmented 8.2.250402
- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250402
- CommunityToolkit.WinUI.Controls.Sizers 8.2.250402
- CommunityToolkit.WinUI.Converters 8.2.250402
- CommunityToolkit.WinUI.Extensions 8.2.250402
- CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2
- CommunityToolkit.WinUI.UI.Controls.Markdown 7.1.2
- ControlzEx 6.0.0
- HelixToolkit 2.24.0
- HelixToolkit.Core.Wpf 2.24.0
- hyjiacan.pinyin4net 4.1.1
- Interop.Microsoft.Office.Interop.OneNote 1.1.0.2
- LazyCache 2.4.0
- Mages 3.0.0
- Markdig.Signed 0.34.0
- MessagePack 3.1.3
- Microsoft.Bcl.AsyncInterfaces 9.0.8
- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
- Microsoft.Data.Sqlite 9.0.8
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
- Microsoft.DotNet.ILCompiler (A)
- Microsoft.Extensions.DependencyInjection 9.0.8
- Microsoft.Extensions.Hosting 9.0.8
- Microsoft.Extensions.Hosting.WindowsServices 9.0.8
- Microsoft.Extensions.Logging 9.0.8
- Microsoft.Extensions.Logging.Abstractions 9.0.8
- Microsoft.NET.ILLink.Tasks (A)
- Microsoft.SemanticKernel 1.15.0
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
- Microsoft.Web.WebView2 1.0.2903.40
- Microsoft.Win32.SystemEvents 9.0.8
- Microsoft.Windows.Compatibility 9.0.8
- Microsoft.Windows.CsWin32 0.3.183
- Microsoft.Windows.CsWinRT 2.2.0
- Microsoft.Windows.SDK.BuildTools 10.0.26100.4188
- Microsoft.WindowsAppSDK 1.7.250513003
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
- ModernWpfUI 0.9.4
- Moq 4.18.4
- MSTest 3.8.3
- NLog.Extensions.Logging 5.3.8
- NLog.Schema 5.2.8
- OpenAI 2.0.0
- ReverseMarkdown 4.1.0
- ScipBe.Common.Office.OneNote 3.0.1
- SharpCompress 0.37.2
- SkiaSharp.Views.WinUI 2.88.9
- StreamJsonRpc 2.21.69
- StyleCop.Analyzers 1.2.0-beta.556
- System.CodeDom 9.0.8
- System.CommandLine 2.0.0-beta4.22272.1
- System.ComponentModel.Composition 9.0.8
- System.Configuration.ConfigurationManager 9.0.8
- System.Data.OleDb 9.0.8
- System.Data.SqlClient 4.9.0
- System.Diagnostics.EventLog 9.0.8
- System.Diagnostics.PerformanceCounter 9.0.8
- System.Drawing.Common 9.0.8
- System.IO.Abstractions 22.0.13
- System.IO.Abstractions.TestingHelpers 22.0.13
- System.Management 9.0.8
- System.Net.Http 4.3.4
- System.Private.Uri 4.3.2
- System.Reactive 6.0.1
- System.Runtime.Caching 9.0.8
- System.ServiceProcess.ServiceController 9.0.8
- System.Text.Encoding.CodePages 9.0.8
- System.Text.Json 9.0.8
- System.Text.RegularExpressions 4.3.1
- UnicodeInformation 2.6.0
- UnitsNet 5.56.0
- UTF.Unknown 2.5.1
- WinUIEx 2.2.0
- WPF-UI 3.0.5
- WyHash 1.0.5

View File

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

View File

@@ -71,41 +71,6 @@ When the user changes settings in the UI:
3. The runner calls the `set_config` function on the appropriate module
4. The module parses the JSON and applies the new settings
# Shortcut Conflict Detection
Steps to enable conflict detection for a hotkey:
### 1. Implement module interface for hotkeys
Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional<HotkeyEx> GetHotkeyEx()`.
- If not yet implemented, you need to add it so that it returns all hotkeys used by the module.
- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup.
- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp`
### 2. Implement IHotkeyConfig in the module settings (UI side)
Make sure the modules settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`.
- This method should return all hotkeys used in the module.
- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`).
- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs`
- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings.
It provides both `getter` and `setter` methods to read and update the corresponding hotkey.
Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey.
This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`
### 3. Update the modules ViewModel
The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()`.
- This method should return all hotkeys, maintaining the same order as in steps 1 and 2.
- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs`
### 4. Ensure the modules Views call `OnPageLoaded()`
Once the modules view is loaded, make sure to invoke the ViewModels `OnPageLoaded()` method:
```cs
Loaded += (s, e) => ViewModel.OnPageLoaded();
```
- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
## Debugging Settings
To debug settings issues:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -112,7 +112,7 @@ private:
return {};
}
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true)
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
{
try
{
@@ -122,7 +122,6 @@ 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 (...)
@@ -232,10 +231,8 @@ private:
return false;
}
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true)
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue)
{
bool actionIsShown = true;
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{
return;
@@ -243,9 +240,9 @@ private:
const auto action = actionValue.GetObjectW();
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown)
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
{
actionIsShown = false;
return;
}
if (action.HasKey(JSON_KEY_SHORTCUT))
@@ -253,7 +250,7 @@ private:
const AdditionalAction additionalAction
{
actionName.c_str(),
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
};
m_additional_actions.push_back(additionalAction);
@@ -262,12 +259,12 @@ private:
{
for (const auto& [subActionName, subAction] : action)
{
process_additional_action(subActionName, subAction, actionIsShown);
process_additional_action(subActionName, subAction);
}
}
}
void read_settings(PowerToysSettings::PowerToyValues& settings)
void read_settings(PowerToysSettings::PowerToyValues& settings)
{
const auto settingsObject = settings.get_raw_json();
@@ -320,21 +317,9 @@ private:
{
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
// 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)
for (const auto& [actionName, additionalAction] : additionalActions)
{
if (additionalActions.HasKey(actionKey))
{
const auto actionValue = additionalActions.GetNamedValue(actionKey);
process_additional_action(actionKey, actionValue);
}
process_additional_action(actionName, additionalAction);
}
}
@@ -346,14 +331,17 @@ private:
for (const auto& customAction : customActions)
{
const auto object = customAction.GetObjectW();
bool actionIsShown = 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), actionIsShown)
};
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))
};
m_custom_actions.push_back(customActionData);
m_custom_actions.push_back(customActionData);
}
}
}
}

View File

@@ -27,73 +27,6 @@ struct InclusiveCrosshairs
void SwitchActivationMode();
void ApplySettings(InclusiveCrosshairsSettings& settings, bool applyToRuntimeObjects);
public:
// Allow external callers to request a position update (thread-safe enqueue)
static void RequestUpdatePosition()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr)
{
instance->UpdateCrosshairsPosition();
}
});
}
}
static void EnsureOn()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr && !instance->m_drawing)
{
instance->StartDrawing();
}
});
}
}
static void EnsureOff()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr && instance->m_drawing)
{
instance->StopDrawing();
}
});
}
}
static void SetExternalControl(bool enabled)
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([enabled]() {
if (instance != nullptr)
{
instance->m_externalControl = enabled;
if (enabled && instance->m_mouseHook)
{
UnhookWindowsHookEx(instance->m_mouseHook);
instance->m_mouseHook = NULL;
}
else if (!enabled && instance->m_drawing && !instance->m_mouseHook)
{
instance->m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, instance->m_hinstance, 0);
}
}
});
}
}
private:
enum class MouseButton
{
@@ -136,7 +69,6 @@ private:
bool m_drawing = false;
bool m_destroyed = false;
bool m_hiddenCursor = false;
bool m_externalControl = false;
void SetAutoHideTimer() noexcept;
// Configurable Settings
@@ -332,12 +264,9 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP
if (nCode >= 0)
{
MSLLHOOKSTRUCT* hookData = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
if (instance && !instance->m_externalControl)
if (wParam == WM_MOUSEMOVE)
{
if (wParam == WM_MOUSEMOVE)
{
instance->UpdateCrosshairsPosition();
}
instance->UpdateCrosshairsPosition();
}
}
return CallNextHookEx(0, nCode, wParam, lParam);
@@ -598,26 +527,6 @@ bool InclusiveCrosshairsIsEnabled()
return (InclusiveCrosshairs::instance != nullptr);
}
void InclusiveCrosshairsRequestUpdatePosition()
{
InclusiveCrosshairs::RequestUpdatePosition();
}
void InclusiveCrosshairsEnsureOn()
{
InclusiveCrosshairs::EnsureOn();
}
void InclusiveCrosshairsEnsureOff()
{
InclusiveCrosshairs::EnsureOff();
}
void InclusiveCrosshairsSetExternalControl(bool enabled)
{
InclusiveCrosshairs::SetExternalControl(enabled);
}
int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings)
{
Logger::info("Starting a crosshairs instance.");

View File

@@ -31,7 +31,3 @@ void InclusiveCrosshairsDisable();
bool InclusiveCrosshairsIsEnabled();
void InclusiveCrosshairsSwitch();
void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings);
void InclusiveCrosshairsRequestUpdatePosition();
void InclusiveCrosshairsEnsureOn();
void InclusiveCrosshairsEnsureOff();
void InclusiveCrosshairsSetExternalControl(bool enabled);

View File

@@ -4,15 +4,6 @@
#include "trace.h"
#include "InclusiveCrosshairs.h"
#include "common/utils/color.h"
#include <atomic>
#include <thread>
#include <chrono>
#include <memory>
extern void InclusiveCrosshairsRequestUpdatePosition();
extern void InclusiveCrosshairsEnsureOn();
extern void InclusiveCrosshairsEnsureOff();
extern void InclusiveCrosshairsSetExternalControl(bool enabled);
// Non-Localizable strings
namespace
@@ -20,7 +11,6 @@ namespace
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
const wchar_t JSON_KEY_GLIDING_ACTIVATION_SHORTCUT[] = L"gliding_cursor_activation_shortcut";
const wchar_t JSON_KEY_CROSSHAIRS_COLOR[] = L"crosshairs_color";
const wchar_t JSON_KEY_CROSSHAIRS_OPACITY[] = L"crosshairs_opacity";
const wchar_t JSON_KEY_CROSSHAIRS_RADIUS[] = L"crosshairs_radius";
@@ -31,15 +21,13 @@ namespace
const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled";
const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_GLIDE_TRAVEL_SPEED[] = L"gliding_travel_speed";
const wchar_t JSON_KEY_GLIDE_DELAY_SPEED[] = L"gliding_delay_speed";
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
HMODULE m_hModule;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
m_hModule = hModule;
switch (ul_reason_for_call)
@@ -69,46 +57,8 @@ private:
// The PowerToy state.
bool m_enabled = false;
// Additional hotkeys (legacy API) to support multiple shortcuts
Hotkey m_activationHotkey{}; // Crosshairs toggle
Hotkey m_glidingHotkey{}; // Gliding cursor state machine
// Shared state for worker threads (decoupled from this lifetime)
struct State
{
std::atomic<bool> stopX{ false };
std::atomic<bool> stopY{ false };
// positions and speeds
int currentXPos{ 0 };
int currentYPos{ 0 };
int currentXSpeed{ 0 }; // pixels per base window
int currentYSpeed{ 0 }; // pixels per base window
int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan
// Fractional accumulators to spread movement across 10ms ticks
double xFraction{ 0.0 };
double yFraction{ 0.0 };
// Speeds represent pixels per 200ms (min 5, max 60 enforced by UI/settings)
int fastHSpeed{ 30 }; // pixels per base window
int slowHSpeed{ 5 }; // pixels per base window
int fastVSpeed{ 30 }; // pixels per base window
int slowVSpeed{ 5 }; // pixels per base window
};
std::shared_ptr<State> m_state;
// Worker threads
std::thread m_xThread;
std::thread m_yThread;
// Gliding cursor state machine
std::atomic<int> m_glideState{ 0 }; // 0..4 like the AHK script
// Timer configuration: 10ms tick, speeds are defined per 200ms base window
static constexpr int kTimerTickMs = 10;
static constexpr int kBaseSpeedTickMs = 200; // mapping period for configured pixel counts
// Hotkey to invoke the module
HotkeyEx m_hotkey;
// Mouse Pointer Crosshairs specific settings
InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings;
@@ -118,17 +68,12 @@ public:
MousePointerCrosshairs()
{
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName);
m_state = std::make_shared<State>();
init_settings();
};
// Destroy the powertoy and free memory
virtual void destroy() override
{
StopXTimer();
StopYTimer();
// Release shared state so worker threads (if any) exit when weak_ptr lock fails
m_state.reset();
delete this;
}
@@ -162,7 +107,9 @@ public:
// Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors.
virtual void call_custom_action(const wchar_t* /*action*/) override {}
virtual void call_custom_action(const wchar_t* action) override
{
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
@@ -196,9 +143,6 @@ public:
{
m_enabled = false;
Trace::EnableMousePointerCrosshairs(false);
StopXTimer();
StopYTimer();
m_glideState = 0;
InclusiveCrosshairsDisable();
}
@@ -214,249 +158,15 @@ public:
return false;
}
// Legacy multi-hotkey support (like CropAndLock)
virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override
virtual std::optional<HotkeyEx> GetHotkeyEx() override
{
if (buffer && buffer_size >= 2)
{
buffer[0] = m_activationHotkey; // Crosshairs toggle
buffer[1] = m_glidingHotkey; // Gliding cursor toggle
}
return 2;
return m_hotkey;
}
virtual bool on_hotkey(size_t hotkeyId) override
virtual void OnHotkeyEx() override
{
if (!m_enabled)
{
return false;
}
if (hotkeyId == 0)
{
InclusiveCrosshairsSwitch();
return true;
}
if (hotkeyId == 1)
{
HandleGlidingHotkey();
return true;
}
return false;
InclusiveCrosshairsSwitch();
}
private:
static void LeftClick()
{
INPUT inputs[2]{};
inputs[0].type = INPUT_MOUSE;
inputs[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(2, inputs, sizeof(INPUT));
}
// Stateless helpers operating on shared State
static void PositionCursorX(const std::shared_ptr<State>& s)
{
int screenW = GetSystemMetrics(SM_CXVIRTUALSCREEN);
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
s->currentYPos = screenH / 2;
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
const double perTick = (static_cast<double>(s->currentXSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
s->xFraction += perTick;
int step = static_cast<int>(s->xFraction);
if (step > 0)
{
s->xFraction -= step;
s->currentXPos += step;
}
s->xPosSnapshot = s->currentXPos;
if (s->currentXPos >= screenW)
{
s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed;
s->xPosSnapshot = 0;
s->xFraction = 0.0; // reset fractional remainder on wrap
}
SetCursorPos(s->currentXPos, s->currentYPos);
// Ensure overlay crosshairs follow immediately
InclusiveCrosshairsRequestUpdatePosition();
}
static void PositionCursorY(const std::shared_ptr<State>& s)
{
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
// Keep X at snapshot
// Use s->xPosSnapshot captured during X pass
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
const double perTick = (static_cast<double>(s->currentYSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
s->yFraction += perTick;
int step = static_cast<int>(s->yFraction);
if (step > 0)
{
s->yFraction -= step;
s->currentYPos += step;
}
if (s->currentYPos >= screenH)
{
s->currentYPos = 0;
s->currentYSpeed = s->fastVSpeed;
s->yFraction = 0.0; // reset fractional remainder on wrap
}
SetCursorPos(s->xPosSnapshot, s->currentYPos);
// Ensure overlay crosshairs follow immediately
InclusiveCrosshairsRequestUpdatePosition();
}
void StartXTimer()
{
auto s = m_state;
if (!s)
{
return;
}
s->stopX = false;
std::weak_ptr<State> wp = s;
m_xThread = std::thread([wp]() {
while (true)
{
auto sp = wp.lock();
if (!sp || sp->stopX.load())
{
break;
}
PositionCursorX(sp);
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
}
});
}
void StopXTimer()
{
auto s = m_state;
if (s)
{
s->stopX = true;
}
if (m_xThread.joinable())
{
m_xThread.join();
}
}
void StartYTimer()
{
auto s = m_state;
if (!s)
{
return;
}
s->stopY = false;
std::weak_ptr<State> wp = s;
m_yThread = std::thread([wp]() {
while (true)
{
auto sp = wp.lock();
if (!sp || sp->stopY.load())
{
break;
}
PositionCursorY(sp);
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
}
});
}
void StopYTimer()
{
auto s = m_state;
if (s)
{
s->stopY = true;
}
if (m_yThread.joinable())
{
m_yThread.join();
}
}
void HandleGlidingHotkey()
{
auto s = m_state;
if (!s)
{
return;
}
// Simulate the AHK state machine
int state = m_glideState.load();
switch (state)
{
case 0:
{
// Ensure crosshairs on (do not toggle off if already on)
InclusiveCrosshairsEnsureOn();
// Disable internal mouse hook so we control position updates explicitly
InclusiveCrosshairsSetExternalControl(true);
s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed;
s->xFraction = 0.0;
s->yFraction = 0.0;
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
SetCursorPos(0, y);
InclusiveCrosshairsRequestUpdatePosition();
m_glideState = 1;
StartXTimer();
break;
}
case 1:
{
// Slow horizontal
s->currentXSpeed = s->slowHSpeed;
m_glideState = 2;
break;
}
case 2:
{
// Stop horizontal, start vertical (fast)
StopXTimer();
s->currentYSpeed = s->fastVSpeed;
s->currentYPos = 0;
s->yFraction = 0.0;
SetCursorPos(s->xPosSnapshot, s->currentYPos);
InclusiveCrosshairsRequestUpdatePosition();
m_glideState = 3;
StartYTimer();
break;
}
case 3:
{
// Slow vertical
s->currentYSpeed = s->slowVSpeed;
m_glideState = 4;
break;
}
case 4:
default:
{
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
StopYTimer();
m_glideState = 0;
LeftClick();
InclusiveCrosshairsEnsureOff();
InclusiveCrosshairsSetExternalControl(false);
s->xFraction = 0.0;
s->yFraction = 0.0;
break;
}
}
}
// Load the settings file.
void init_settings()
{
@@ -482,44 +192,37 @@ private:
{
try
{
// Parse primary activation HotKey (for centralized hook)
// Parse HotKey
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
m_hotkey = HotkeyEx();
if (hotkey.win_pressed())
{
m_hotkey.modifiersMask |= MOD_WIN;
}
// Map to legacy Hotkey for multi-hotkey API
m_activationHotkey.win = hotkey.win_pressed();
m_activationHotkey.ctrl = hotkey.ctrl_pressed();
m_activationHotkey.shift = hotkey.shift_pressed();
m_activationHotkey.alt = hotkey.alt_pressed();
m_activationHotkey.key = static_cast<unsigned char>(hotkey.get_code());
if (hotkey.ctrl_pressed())
{
m_hotkey.modifiersMask |= MOD_CONTROL;
}
if (hotkey.shift_pressed())
{
m_hotkey.modifiersMask |= MOD_SHIFT;
}
if (hotkey.alt_pressed())
{
m_hotkey.modifiersMask |= MOD_ALT;
}
m_hotkey.vkCode = hotkey.get_code();
}
catch (...)
{
Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
}
try
{
// Parse Gliding Cursor HotKey
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
m_glidingHotkey.win = hotkey.win_pressed();
m_glidingHotkey.ctrl = hotkey.ctrl_pressed();
m_glidingHotkey.shift = hotkey.shift_pressed();
m_glidingHotkey.alt = hotkey.alt_pressed();
m_glidingHotkey.key = static_cast<unsigned char>(hotkey.get_code());
}
catch (...)
{
// note that this is also defined in src\settings-ui\Settings.UI.Library\MousePointerCrosshairsProperties.cs, DefaultGlidingCursorActivationShortcut
// both need to be kept in sync!
Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+.");
m_glidingHotkey.win = true;
m_glidingHotkey.alt = true;
m_glidingHotkey.ctrl = false;
m_glidingHotkey.shift = false;
m_glidingHotkey.key = VK_OEM_PERIOD;
}
try
{
// Parse Opacity
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY);
@@ -569,6 +272,7 @@ private:
{
throw std::runtime_error("Invalid Radius value");
}
}
catch (...)
{
@@ -587,6 +291,7 @@ private:
{
throw std::runtime_error("Invalid Thickness value");
}
}
catch (...)
{
@@ -615,7 +320,7 @@ private:
{
// Parse border size
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
int value = static_cast <int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
inclusiveCrosshairsSettings.crosshairsBorderSize = value;
@@ -678,86 +383,20 @@ private:
{
Logger::warn("Failed to initialize auto activate from settings. Will use default value");
}
try
{
// Parse Travel speed (fast speed mapping)
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_TRAVEL_SPEED);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 5 && value <= 60)
{
m_state->fastHSpeed = value;
m_state->fastVSpeed = value;
}
else if (value < 5)
{
m_state->fastHSpeed = 5; m_state->fastVSpeed = 5;
}
else
{
m_state->fastHSpeed = 60; m_state->fastVSpeed = 60;
}
}
catch (...)
{
Logger::warn("Failed to initialize gliding travel speed from settings. Using default 25.");
if (m_state)
{
m_state->fastHSpeed = 25;
m_state->fastVSpeed = 25;
}
}
try
{
// Parse Delay speed (slow speed mapping)
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_DELAY_SPEED);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 5 && value <= 60)
{
m_state->slowHSpeed = value;
m_state->slowVSpeed = value;
}
else if (value < 5)
{
m_state->slowHSpeed = 5; m_state->slowVSpeed = 5;
}
else
{
m_state->slowHSpeed = 60; m_state->slowVSpeed = 60;
}
}
catch (...)
{
Logger::warn("Failed to initialize gliding delay speed from settings. Using default 5.");
if (m_state)
{
m_state->slowHSpeed = 5;
m_state->slowVSpeed = 5;
}
}
}
else
{
Logger::info("Mouse Pointer Crosshairs settings are empty");
}
if (m_activationHotkey.key == 0)
if (!m_hotkey.modifiersMask)
{
m_activationHotkey.win = true;
m_activationHotkey.alt = true;
m_activationHotkey.ctrl = false;
m_activationHotkey.shift = false;
m_activationHotkey.key = 'P';
}
if (m_glidingHotkey.key == 0)
{
m_glidingHotkey.win = true;
m_glidingHotkey.alt = true;
m_glidingHotkey.ctrl = false;
m_glidingHotkey.shift = false;
m_glidingHotkey.key = VK_OEM_PERIOD;
Logger::info("Mouse Pointer Crosshairs is going to use default shortcut");
m_hotkey.modifiersMask = MOD_WIN | MOD_ALT;
m_hotkey.vkCode = 0x50; // P key
}
m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings;
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()

View File

@@ -556,61 +556,6 @@ public:
return m_enabled;
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
if (hotkeys && buffer_size >= num_hotkeys)
{
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
// Cache the raw JSON object to avoid multiple parsing
json::JsonObject root_json = values.get_raw_json();
json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
size_t hotkey_index = 0;
// Helper lambda to extract hotkey from JSON properties
auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey {
if (properties_json.HasKey(property_name))
{
try
{
json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
// Extract hotkey properties directly from JSON
bool win = hotkey_json.GetNamedBoolean(L"win", false);
bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false);
bool alt = hotkey_json.GetNamedBoolean(L"alt", false);
bool shift = hotkey_json.GetNamedBoolean(L"shift", false);
unsigned char key = static_cast<unsigned char>(
hotkey_json.GetNamedNumber(L"code", 0));
return { win, ctrl, shift, alt, key };
}
catch (...)
{
// If parsing individual hotkey fails, use defaults
return { false, false, false, false, 0 };
}
}
else
{
// Property doesn't exist, use defaults
return { false, false, false, false, 0 };
}
};
// Extract all hotkeys using the optimized helper
hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse
hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine
hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs
hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect
}
return num_hotkeys;
}
void launch_add_firewall_process()
{
Logger::trace(L"Starting Process to add firewall rule");

View File

@@ -141,9 +141,6 @@ 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
@@ -154,6 +151,9 @@ 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,22 +187,11 @@ 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, 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();
ListHelpers.InPlaceUpdateList(Items, newViewModels);
}
// TODO: Iterate over everything in Items, and prune items from the
@@ -211,15 +200,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
catch (OperationCanceledException)
{
// Cancellation is expected, don't treat as error
// However, if we were cancelled, we didn't actually add these items to
// our Items list. Before we release them to the GC, make sure we clean
// them up
foreach (var vm in newViewModels)
{
vm.SafeCleanup();
}
return;
}
catch (Exception ex)

View File

@@ -2,7 +2,6 @@
// 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;
@@ -97,40 +96,109 @@ 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)
{
if (action is AdaptiveOpenUrlAction openUrlAction)
{
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
return;
}
// 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 (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
if (actionJson.TryGetValue("type", out var actionTypeValue))
{
// Get the data and inputs
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
var inputString = inputs.Stringify();
var actionTypeString = actionTypeValue.GetString();
Logger.LogTrace($"atString={actionTypeString}");
_ = Task.Run(() =>
var actionType = actionTypeString switch
{
try
{
var model = _formModel.Unsafe!;
if (model != null)
"Action.Submit" => ActionType.Submit,
"Action.Execute" => ActionType.Execute,
"Action.OpenUrl" => ActionType.OpenUrl,
_ => ActionType.Unsupported,
};
Logger.LogDebug($"{actionTypeString}->{actionType}");
switch (actionType)
{
case ActionType.OpenUrl:
{
var result = model.SubmitForm(inputString, dataString);
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
HandleOpenUrlAction(action, actionJson);
}
}
catch (Exception ex)
{
ShowException(ex);
}
});
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;
}
var inputString = inputs.Stringify();
_ = Task.Run(() =>
{
try
{
var model = _formModel.Unsafe!;
if (model != null)
{
var result = model.SubmitForm(inputString, dataString);
Logger.LogDebug($"SubmitForm() returned {result}");
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
}
}
catch (Exception ex)
{
ShowException(ex);
}
});
}
private static readonly string ErrorCardJson = """

View File

@@ -41,18 +41,14 @@
<Style.Setters>
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
</Style.Setters>
</Style>
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
<Flyout
x:Name="ContextMenuFlyout"
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
Opened="ContextMenuFlyout_Opened"
ShouldConstrainToRootBounds="False"
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
ShouldConstrainToRootBounds="False">
<cpcontrols:ContextMenu x:Name="ContextControl" />
</Flyout>

View File

@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.Extensions.DependencyInjection;
@@ -162,11 +163,11 @@ public sealed partial class ListPage : Page,
if (listViewPeer is not null && li is not null)
{
var notificationText = li.Title;
listViewPeer.RaiseNotificationEvent(
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationKind.Other,
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationProcessing.MostRecent,
notificationText,
"CommandPaletteSelectedItemChanged");
UIHelper.AnnounceActionForAccessibility(
ItemsList,
notificationText,
"CommandPaletteSelectedItemChanged");
}
}
}

View File

@@ -92,7 +92,7 @@ internal sealed partial class TrayIconService
{
_popupMenu = PInvoke.CreatePopupMenu_SafeHandle();
PInvoke.InsertMenu(_popupMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 1, RS_.GetString("TrayMenu_Settings"));
PInvoke.InsertMenu(_popupMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 2, RS_.GetString("TrayMenu_Close"));
PInvoke.InsertMenu(_popupMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 2, RS_.GetString("TrayMenu_Exit"));
}
}
else

View File

@@ -0,0 +1,32 @@
// 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.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
namespace Microsoft.CmdPal.UI.Helpers;
public static partial class UIHelper
{
static UIHelper()
{
}
public static void AnnounceActionForAccessibility(UIElement ue, string announcement, string activityID)
{
if (FrameworkElementAutomationPeer.FromElement(ue) is AutomationPeer peer)
{
peer.RaiseNotificationEvent(
AutomationNotificationKind.ActionCompleted,
AutomationNotificationProcessing.ImportantMostRecent,
announcement,
activityID);
}
}
}

View File

@@ -25,17 +25,11 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- TO DO: Replace this with WinUI TitleBar once that ships. -->
<Button
x:Name="PaneToggleBtn"
Width="48"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Click="PaneToggleBtn_Click"
Style="{StaticResource PaneToggleButtonStyle}" />
<StackPanel
x:Name="AppTitleBar"
Grid.Row="0"
Height="48"
Margin="16,0,0,0"
Orientation="Horizontal">
<Image
Width="16"

View File

@@ -10,6 +10,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
@@ -22,6 +23,9 @@ public sealed partial class SettingsWindow : WindowEx,
{
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
// Gets or sets optional action invoked after NavigationView is loaded.
public Action NavigationViewLoaded { get; set; } = () => { };
public SettingsWindow()
{
this.InitializeComponent();
@@ -35,10 +39,33 @@ public sealed partial class SettingsWindow : WindowEx,
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
}
// Handles NavigationView loaded event.
// Sets up initial navigation and accessibility notifications.
private void NavView_Loaded(object sender, RoutedEventArgs e)
{
// Delay necessary to ensure NavigationView visual state can match navigation
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
NavView.SelectedItem = NavView.MenuItems[0];
Navigate("General");
if (sender is NavigationView navigationView)
{
// Register for pane open/close changes to announce to screen readers
navigationView.RegisterPropertyChangedCallback(NavigationView.IsPaneOpenProperty, AnnounceNavigationPaneStateChanged);
}
}
// Announces navigation pane open/close state to screen readers for accessibility.
private void AnnounceNavigationPaneStateChanged(DependencyObject sender, DependencyProperty dp)
{
if (sender is NavigationView navigationView)
{
UIHelper.AnnounceActionForAccessibility(
ue: (UIElement)sender,
(sender as NavigationView)?.IsPaneOpen == true ? RS_.GetString("NavigationPaneOpened") : RS_.GetString("NavigationPaneClosed"),
"NavigationViewPaneIsOpenChangeNotificationId");
}
}
private void NavView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args)
@@ -109,24 +136,15 @@ public sealed partial class SettingsWindow : WindowEx,
WeakReferenceMessenger.Default.UnregisterAll(this);
}
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)
{
NavView.IsPaneOpen = !NavView.IsPaneOpen;
}
private void NavView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
{
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
{
PaneToggleBtn.Visibility = Visibility.Visible;
NavView.IsPaneToggleButtonVisible = false;
AppTitleBar.Margin = new Thickness(48, 0, 0, 0);
}
else
{
PaneToggleBtn.Visibility = Visibility.Collapsed;
NavView.IsPaneToggleButtonVisible = true;
AppTitleBar.Margin = new Thickness(16, 0, 0, 0);
}
}

View File

@@ -419,9 +419,8 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="TrayMenu_Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="TrayMenu_Close" xml:space="preserve">
<value>Close</value>
<comment>Close as a verb, as in Close the application</comment>
<data name="TrayMenu_Exit" xml:space="preserve">
<value>Exit</value>
</data>
<data name="Settings_ExtensionPage_Alias_ToggleSwitch.OnContent" xml:space="preserve">
<value>Direct</value>
@@ -435,4 +434,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="StatusMessagesButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Show status messages</value>
</data>
<data name="NavigationPaneClosed" xml:space="preserve">
<value>Navigation pane closed</value>
</data>
<data name="NavigationPageOpened" xml:space="preserve">
<value>Navigation page opened</value>
</data>
</root>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,45 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[TestMethod]
public void QueryReturnsExpectedResults()
{
// Arrange
var mockCache = new MockAppCache();
var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
mockCache.AddWin32Program(win32App);
mockCache.AddUWPApplication(uwpApp);
for (var i = 0; i < 10; i++)
{
mockCache.AddWin32Program(TestDataHelper.CreateTestWin32Program($"App{i}"));
mockCache.AddUWPApplication(TestDataHelper.CreateTestUWPApplication($"UWP App {i}"));
}
var page = new AllAppsPage(mockCache);
var provider = new AllAppsCommandProvider(page);
// Act
var allItems = page.GetItems();
// Assert
var notepadResult = Query("notepad", allItems).FirstOrDefault();
Assert.IsNotNull(notepadResult);
Assert.AreEqual("Notepad", notepadResult.Title);
var calculatorResult = Query("cal", allItems).FirstOrDefault();
Assert.IsNotNull(calculatorResult);
Assert.AreEqual("Calculator", calculatorResult.Title);
}
}

View File

@@ -1,58 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Apps.Helpers;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
public class Settings : ISettingsInterface
{
private readonly bool enableStartMenuSource;
private readonly bool enableDesktopSource;
private readonly bool enableRegistrySource;
private readonly bool enablePathEnvironmentVariableSource;
private readonly List<string> programSuffixes;
private readonly List<string> runCommandSuffixes;
public Settings(
bool enableStartMenuSource = true,
bool enableDesktopSource = true,
bool enableRegistrySource = true,
bool enablePathEnvironmentVariableSource = true,
List<string> programSuffixes = null,
List<string> runCommandSuffixes = null)
{
this.enableStartMenuSource = enableStartMenuSource;
this.enableDesktopSource = enableDesktopSource;
this.enableRegistrySource = enableRegistrySource;
this.enablePathEnvironmentVariableSource = enablePathEnvironmentVariableSource;
this.programSuffixes = programSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url" };
this.runCommandSuffixes = runCommandSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url", "cpl", "msc" };
}
public bool EnableStartMenuSource => enableStartMenuSource;
public bool EnableDesktopSource => enableDesktopSource;
public bool EnableRegistrySource => enableRegistrySource;
public bool EnablePathEnvironmentVariableSource => enablePathEnvironmentVariableSource;
public List<string> ProgramSuffixes => programSuffixes;
public List<string> RunCommandSuffixes => runCommandSuffixes;
public static Settings CreateDefaultSettings() => new Settings();
public static Settings CreateDisabledSourcesSettings() => new Settings(
enableStartMenuSource: false,
enableDesktopSource: false,
enableRegistrySource: false,
enablePathEnvironmentVariableSource: false);
public static Settings CreateCustomSuffixesSettings() => new Settings(
programSuffixes: new List<string> { "exe", "bat" },
runCommandSuffixes: new List<string> { "exe", "bat", "cmd" });
}

View File

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

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarkDataTests
{
[TestMethod]
public void BookmarkDataWebUrlDetection()
{
// Act
var webBookmark = new BookmarkData
{
Name = "Test Site",
Bookmark = "https://test.com",
};
var nonWebBookmark = new BookmarkData
{
Name = "Local File",
Bookmark = "C:\\temp\\file.txt",
};
var placeholderBookmark = new BookmarkData
{
Name = "Placeholder",
Bookmark = "{Placeholder}",
};
// Assert
Assert.IsTrue(webBookmark.IsWebUrl());
Assert.IsFalse(webBookmark.IsPlaceholder);
Assert.IsFalse(nonWebBookmark.IsWebUrl());
Assert.IsFalse(nonWebBookmark.IsPlaceholder);
Assert.IsTrue(placeholderBookmark.IsPlaceholder);
}
}

View File

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

View File

@@ -1,137 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarksCommandProviderTests
{
[TestMethod]
public void ProviderHasCorrectId()
{
// Setup
var mockDataSource = new MockBookmarkDataSource();
var provider = new BookmarksCommandProvider(mockDataSource);
// Assert
Assert.AreEqual("Bookmarks", provider.Id);
}
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var mockDataSource = new MockBookmarkDataSource();
var provider = new BookmarksCommandProvider(mockDataSource);
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new BookmarksCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new BookmarksCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void ProviderWithMockData_LoadsBookmarksCorrectly()
{
// Arrange
var jsonData = @"{
""Data"": [
{
""Name"": ""Test Bookmark"",
""Bookmark"": ""https://test.com""
},
{
""Name"": ""Another Bookmark"",
""Bookmark"": ""https://another.com""
}
]
}";
var dataSource = new MockBookmarkDataSource(jsonData);
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
var testBookmark = commands.Where(c => c.Title.Contains("Test Bookmark")).FirstOrDefault();
// Should have three commandsAdd + two custom bookmarks
Assert.AreEqual(3, commands.Length);
Assert.IsNotNull(addCommand);
Assert.IsNotNull(testBookmark);
}
[TestMethod]
public void ProviderWithEmptyData_HasOnlyAddCommand()
{
// Arrange
var dataSource = new MockBookmarkDataSource(@"{ ""Data"": [] }");
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
// Only have Add command
Assert.AreEqual(1, commands.Length);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
Assert.IsNotNull(addCommand);
}
[TestMethod]
public void ProviderWithInvalidData_HandlesGracefully()
{
// Arrange
var dataSource = new MockBookmarkDataSource("invalid json");
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
// Only have one command. Will ignore json parse error.
Assert.AreEqual(1, commands.Length);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
Assert.IsNotNull(addCommand);
}
}

View File

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

View File

@@ -1,24 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
internal sealed class MockBookmarkDataSource : IBookmarkDataSource
{
private string _jsonData;
public MockBookmarkDataSource(string initialJsonData = "[]")
{
_jsonData = initialJsonData;
}
public string GetBookmarkData()
{
return _jsonData;
}
public void SaveBookmarkData(string jsonData)
{
_jsonData = jsonData;
}
}

View File

@@ -1,55 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[TestMethod]
public void ValidateBookmarksCreation()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
// Assert
Assert.IsNotNull(bookmarks);
Assert.IsNotNull(bookmarks.Data);
Assert.AreEqual(2, bookmarks.Data.Count);
}
[TestMethod]
public void ValidateBookmarkData()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
// Act
var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
var githubBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "GitHub");
// Assert
Assert.IsNotNull(microsoftBookmark);
Assert.AreEqual("https://www.microsoft.com", microsoftBookmark.Bookmark);
Assert.IsNotNull(githubBookmark);
Assert.AreEqual("https://github.com", githubBookmark.Bookmark);
}
[TestMethod]
public void ValidateWebUrlDetection()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
// Assert
Assert.IsNotNull(microsoftBookmark);
Assert.IsTrue(microsoftBookmark.IsWebUrl());
}
}

View File

@@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
public static class Settings
{
public static Bookmarks CreateDefaultBookmarks()
{
var bookmarks = new Bookmarks();
// Add some test bookmarks
bookmarks.Data.Add(new BookmarkData
{
Name = "Microsoft",
Bookmark = "https://www.microsoft.com",
});
bookmarks.Data.Add(new BookmarkData
{
Name = "GitHub",
Bookmark = "https://github.com",
});
return bookmarks;
}
}

View File

@@ -1,60 +0,0 @@
// 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.Globalization;
using System.Linq;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class CloseOnEnterTests
{
[TestMethod]
public void PrimaryIsCopy_WhenCloseOnEnterTrue()
{
var settings = new Settings(closeOnEnter: true);
TypedEventHandler<object, object> handleSave = (s, e) => { };
var item = ResultHelper.CreateResult(
4m,
CultureInfo.CurrentCulture,
CultureInfo.CurrentCulture,
"2+2",
settings,
handleSave);
Assert.IsNotNull(item);
Assert.IsInstanceOfType(item.Command, typeof(CopyTextCommand));
var firstMore = item.MoreCommands.First();
Assert.IsInstanceOfType(firstMore, typeof(CommandContextItem));
Assert.IsInstanceOfType(((CommandItem)firstMore).Command, typeof(SaveCommand));
}
[TestMethod]
public void PrimaryIsSave_WhenCloseOnEnterFalse()
{
var settings = new Settings(closeOnEnter: false);
TypedEventHandler<object, object> handleSave = (s, e) => { };
var item = ResultHelper.CreateResult(
4m,
CultureInfo.CurrentCulture,
CultureInfo.CurrentCulture,
"2+2",
settings,
handleSave);
Assert.IsNotNull(item);
Assert.IsInstanceOfType(item.Command, typeof(SaveCommand));
var firstMore = item.MoreCommands.First();
Assert.IsInstanceOfType(firstMore, typeof(CommandContextItem));
Assert.IsInstanceOfType(((CommandItem)firstMore).Command, typeof(CopyTextCommand));
}
}

View File

@@ -19,23 +19,16 @@ 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)],
@@ -45,11 +38,11 @@ public partial class AllAppsCommandProvider : CommandProvider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
}
public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
public override ICommandItem[] TopLevelCommands() => [_listItem, ..Page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName)
{
var items = _page.GetItems();
var items = Page.GetItems();
// We're going to do this search in two directions:
// First, is this name a substring of any app...

View File

@@ -2,7 +2,6 @@
// 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;
@@ -20,20 +19,13 @@ 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;
@@ -67,7 +59,7 @@ public sealed partial class AllAppsPage : ListPage
private void BuildListItems()
{
if (allApps.Length == 0 || _appCache.ShouldReload())
if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload())
{
lock (_listLock)
{
@@ -83,7 +75,7 @@ public sealed partial class AllAppsPage : ListPage
this.IsLoading = false;
_appCache.ResetReloadFlag();
AppCache.Instance.Value.ResetReloadFlag();
stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
@@ -93,11 +85,11 @@ public sealed partial class AllAppsPage : ListPage
private AppItem[] GetAllApps()
{
var uwpResults = _appCache.UWPs
var uwpResults = AppCache.Instance.Value.UWPs
.Where((application) => application.Enabled)
.Select(app => app.ToAppItem());
var win32Results = _appCache.Win32s
var win32Results = AppCache.Instance.Value.Win32s
.Where((application) => application.Enabled && application.Valid)
.Select(app => app.ToAppItem());

View File

@@ -5,14 +5,13 @@
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, ISettingsInterface
public class AllAppsSettings : JsonSettingsManager
{
private static readonly string _namespace = "apps";

View File

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

View File

@@ -26,11 +26,6 @@ public sealed partial class AppCommand : InvokableCommand
Name = Resources.run_command_action;
Id = GenerateId();
if (!string.IsNullOrEmpty(app.IcoPath))
{
Icon = new(app.IcoPath);
}
}
internal static async Task StartApp(string aumid)

View File

@@ -1,22 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
public interface ISettingsInterface
{
public bool EnableStartMenuSource { get; }
public bool EnableDesktopSource { get; }
public bool EnableRegistrySource { get; }
public bool EnablePathEnvironmentVariableSource { get; }
public List<string> ProgramSuffixes { get; }
public List<string> RunCommandSuffixes { get; }
}

View File

@@ -1,37 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps;
/// <summary>
/// Interface for application cache that provides access to Win32 and UWP applications.
/// </summary>
public interface IAppCache : IDisposable
{
/// <summary>
/// Gets the collection of Win32 programs.
/// </summary>
IList<Win32Program> Win32s { get; }
/// <summary>
/// Gets the collection of UWP applications.
/// </summary>
IList<IUWPApplication> UWPs { get; }
/// <summary>
/// Determines whether the cache should be reloaded.
/// </summary>
/// <returns>True if cache should be reloaded, false otherwise.</returns>
bool ShouldReload();
/// <summary>
/// Resets the reload flag.
/// </summary>
void ResetReloadFlag();
}

View File

@@ -1,43 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
/// <summary>
/// Interface for UWP applications to enable testing and mocking
/// </summary>
public interface IUWPApplication : IProgram
{
string AppListEntry { get; set; }
string DisplayName { get; set; }
string UserModelId { get; set; }
string BackgroundColor { get; set; }
string EntryPoint { get; set; }
bool CanRunElevated { get; set; }
string LogoPath { get; set; }
LogoType LogoType { get; set; }
UWP Package { get; set; }
string LocationLocalized { get; }
string GetAppIdentifier();
List<IContextItem> GetCommands();
void UpdateLogoPath(Utils.Theme theme);
AppItem ToAppItem();
}

View File

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

View File

@@ -1,7 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Apps.UnitTests")]

View File

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

View File

@@ -1,45 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class BookmarkJsonParser
{
public BookmarkJsonParser()
{
}
public Bookmarks ParseBookmarks(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return new Bookmarks();
}
try
{
var bookmarks = JsonSerializer.Deserialize<Bookmarks>(json, BookmarkSerializationContext.Default.Bookmarks);
return bookmarks ?? new Bookmarks();
}
catch (JsonException ex)
{
ExtensionHost.LogMessage($"parse bookmark data failed. ex: {ex.Message}");
return new Bookmarks();
}
}
public string SerializeBookmarks(Bookmarks? bookmarks)
{
if (bookmarks == null)
{
return string.Empty;
}
return JsonSerializer.Serialize(bookmarks, BookmarkSerializationContext.Default.Bookmarks);
}
}

View File

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

View File

@@ -20,20 +20,10 @@ 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;
@@ -59,14 +49,10 @@ public partial class BookmarksCommandProvider : CommandProvider
private void SaveAndUpdateCommands()
{
try
if (_bookmarks is not null)
{
var jsonData = _parser.SerializeBookmarks(_bookmarks);
_dataSource.SaveBookmarkData(jsonData);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save bookmarks: {ex.Message}");
var jsonPath = BookmarksCommandProvider.StateJsonPath();
Bookmarks.WriteToFile(jsonPath, _bookmarks);
}
LoadCommands();
@@ -96,8 +82,11 @@ public partial class BookmarksCommandProvider : CommandProvider
{
try
{
var jsonData = _dataSource.GetBookmarkData();
_bookmarks = _parser.ParseBookmarks(jsonData);
var jsonFile = StateJsonPath();
if (File.Exists(jsonFile))
{
_bookmarks = Bookmarks.ReadFromFile(jsonFile);
}
}
catch (Exception ex)
{

View File

@@ -1,49 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class FileBookmarkDataSource : IBookmarkDataSource
{
private readonly string _filePath;
public FileBookmarkDataSource(string filePath)
{
_filePath = filePath;
}
public string GetBookmarkData()
{
if (!File.Exists(_filePath))
{
return string.Empty;
}
try
{
return File.ReadAllText(_filePath);
}
catch (Exception ex)
{
ExtensionHost.LogMessage($"Read bookmark data failed. ex: {ex.Message}");
return string.Empty;
}
}
public void SaveBookmarkData(string jsonData)
{
try
{
File.WriteAllText(_filePath, jsonData);
}
catch (Exception ex)
{
ExtensionHost.LogMessage($"Failed to save bookmark data: {ex}");
}
}
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.Bookmarks;
public interface IBookmarkDataSource
{
string GetBookmarkData();
void SaveBookmarkData(string jsonData);
}

View File

@@ -1,7 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Bookmarks.UnitTests")]

View File

@@ -8,6 +8,7 @@ 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;
@@ -15,6 +16,7 @@ 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;
@@ -52,6 +54,14 @@ 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
});
}
@@ -60,4 +70,17 @@ internal sealed partial class ProfilesListPage : ListPage
}
public override IListItem[] GetItems() => Query().ToArray();
private BitmapImage GetLogo(TerminalPackage terminal)
{
var aumid = terminal.AppUserModelId;
if (!_logoCache.TryGetValue(aumid, out var value))
{
value = terminal.GetLogo();
_logoCache.Add(aumid, value);
}
return value;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,32 +65,12 @@ 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;
}
@@ -112,7 +92,6 @@ 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);
}
@@ -141,7 +120,6 @@ public partial class ListHelpers
while (original.Count > numberOfNew)
{
// RemoveAtEnd
removedItems.Add(original[original.Count - 1]);
original.RemoveAt(original.Count - 1);
}

View File

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

View File

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

View File

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

View File

@@ -45,44 +45,14 @@ 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& 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;
}
std::strong_ordering operator<=>(const Hotkey&) const = default;
};
struct HotkeyEx
{
WORD modifiersMask = 0;
WORD vkCode = 0;
int id = 0;
};
/* Returns the localized name of the PowerToy*/

View File

@@ -30,7 +30,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NLog" />
<PackageReference Include="NLog.Extensions.Logging" />
<!-- HACK: Microsoft.Extensions.Hosting is referenced, even if it is not used, to force dll versions to be the same as in other projects. Really only needed since OneNote uses the LazyCache and NLog dependencies, which references older assemblies. -->
<PackageReference Include="Microsoft.Extensions.Hosting" />

View File

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

View File

@@ -176,6 +176,10 @@
<data name="DOCUMENTATION_MENU_TEXT" xml:space="preserve">
<value>Documentation</value>
</data>
<data name="EXIT_MENU_TEXT" xml:space="preserve">
<value>Exit</value>
<comment>Exit as a verb, as in Exit the application</comment>
</data>
<data name="SUBMIT_BUG_TEXT" xml:space="preserve">
<value>Report bug</value>
</data>
@@ -189,8 +193,4 @@
<data name="TRAY_ICON_ADMIN_TOOLTIP" xml:space="preserve">
<value>Administrator</value>
</data>
<data name="CLOSE_MENU_TEXT" xml:space="preserve">
<value>Close</value>
<comment>Close as a verb, as in Close the application</comment>
</data>
</root>

View File

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

View File

@@ -3,7 +3,6 @@
#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"
@@ -205,15 +204,11 @@ 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();
@@ -320,8 +315,6 @@ void start_enabled_powertoys()
{
Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name);
powertoy->enable();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.EnableHotkeyByModule(name);
powertoy.UpdateHotkeyEx();
}
}

View File

@@ -1,471 +0,0 @@
#include "pch.h"
#include "hotkey_conflict_detector.h"
#include <common/SettingsAPI/settings_helpers.h>
#include <windows.h>
#include <unordered_map>
#include <cwchar>
namespace HotkeyConflictDetector
{
Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut)
{
Hotkey hotkey;
hotkey.win = (shortcut.modifiersMask & MOD_WIN) != 0;
hotkey.ctrl = (shortcut.modifiersMask & MOD_CONTROL) != 0;
hotkey.shift = (shortcut.modifiersMask & MOD_SHIFT) != 0;
hotkey.alt = (shortcut.modifiersMask & MOD_ALT) != 0;
hotkey.key = shortcut.vkCode > 255 ? 0 : static_cast<unsigned char>(shortcut.vkCode);
return hotkey;
}
HotkeyConflictManager* HotkeyConflictManager::instance = nullptr;
std::mutex HotkeyConflictManager::instanceMutex;
HotkeyConflictManager& HotkeyConflictManager::GetInstance()
{
std::lock_guard<std::mutex> lock(instanceMutex);
if (instance == nullptr)
{
instance = new HotkeyConflictManager();
}
return *instance;
}
HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID)
{
if (disabledHotkeys.find(_moduleName) != disabledHotkeys.end())
{
return HotkeyConflictType::NoConflict;
}
uint16_t handle = GetHotkeyHandle(_hotkey);
if (handle == 0)
{
return HotkeyConflictType::NoConflict;
}
// The order is important, first to check sys conflict and then inapp conflict
if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
{
return HotkeyConflictType::SystemConflict;
}
if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
{
return HotkeyConflictType::InAppConflict;
}
auto it = hotkeyMap.find(handle);
if (it == hotkeyMap.end())
{
return HasConflictWithSystemHotkey(_hotkey) ?
HotkeyConflictType::SystemConflict :
HotkeyConflictType::NoConflict;
}
if (wcscmp(it->second.moduleName.c_str(), _moduleName) == 0 && it->second.hotkeyID == _hotkeyID)
{
// A shortcut matching its own assignment is not considered a conflict.
return HotkeyConflictType::NoConflict;
}
return HotkeyConflictType::InAppConflict;
}
HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey)
{
uint16_t handle = GetHotkeyHandle(_hotkey);
if (handle == 0)
{
return HotkeyConflictType::NoConflict;
}
// The order is important, first to check sys conflict and then inapp conflict
if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
{
return HotkeyConflictType::SystemConflict;
}
if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
{
return HotkeyConflictType::InAppConflict;
}
auto it = hotkeyMap.find(handle);
if (it == hotkeyMap.end())
{
return HasConflictWithSystemHotkey(_hotkey) ?
HotkeyConflictType::SystemConflict :
HotkeyConflictType::NoConflict;
}
return HotkeyConflictType::InAppConflict;
}
// This function should only be called when a conflict has already been identified.
// It returns a list of all conflicting shortcuts.
std::vector<HotkeyConflictInfo> HotkeyConflictManager::GetAllConflicts(Hotkey const& _hotkey)
{
std::vector<HotkeyConflictInfo> conflicts;
uint16_t handle = GetHotkeyHandle(_hotkey);
// Check in-app conflicts first
auto inAppIt = inAppConflictHotkeyMap.find(handle);
if (inAppIt != inAppConflictHotkeyMap.end())
{
// Add all in-app conflicts
for (const auto& conflict : inAppIt->second)
{
conflicts.push_back(conflict);
}
return conflicts;
}
// Check system conflicts
auto sysIt = sysConflictHotkeyMap.find(handle);
if (sysIt != sysConflictHotkeyMap.end())
{
HotkeyConflictInfo systemConflict;
systemConflict.hotkey = _hotkey;
systemConflict.moduleName = L"System";
systemConflict.hotkeyID = 0;
conflicts.push_back(systemConflict);
return conflicts;
}
// Check if there's a successfully registered hotkey that would conflict
auto registeredIt = hotkeyMap.find(handle);
if (registeredIt != hotkeyMap.end())
{
conflicts.push_back(registeredIt->second);
return conflicts;
}
// If all the above conditions are ruled out, a system-level conflict is the only remaining explanation.
HotkeyConflictInfo systemConflict;
systemConflict.hotkey = _hotkey;
systemConflict.moduleName = L"System";
systemConflict.hotkeyID = 0;
conflicts.push_back(systemConflict);
return conflicts;
}
bool HotkeyConflictManager::AddHotkey(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID, bool isEnabled)
{
if (!isEnabled)
{
disabledHotkeys[_moduleName].push_back({ _hotkey, _moduleName, _hotkeyID });
return true;
}
uint16_t handle = GetHotkeyHandle(_hotkey);
if (handle == 0)
{
return false;
}
HotkeyConflictType conflictType = HasConflict(_hotkey, _moduleName, _hotkeyID);
if (conflictType != HotkeyConflictType::NoConflict)
{
if (conflictType == HotkeyConflictType::InAppConflict)
{
auto hotkeyFound = hotkeyMap.find(handle);
inAppConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID });
if (hotkeyFound != hotkeyMap.end())
{
inAppConflictHotkeyMap[handle].insert(hotkeyFound->second);
hotkeyMap.erase(hotkeyFound);
}
}
else
{
sysConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID });
}
return false;
}
HotkeyConflictInfo hotkeyInfo;
hotkeyInfo.moduleName = _moduleName;
hotkeyInfo.hotkeyID = _hotkeyID;
hotkeyInfo.hotkey = _hotkey;
hotkeyMap[handle] = hotkeyInfo;
return true;
}
std::vector<HotkeyConflictInfo> HotkeyConflictManager::RemoveHotkeyByModule(const std::wstring& moduleName)
{
std::vector<HotkeyConflictInfo> removedHotkeys;
if (disabledHotkeys.find(moduleName) != disabledHotkeys.end())
{
disabledHotkeys.erase(moduleName);
}
std::lock_guard<std::mutex> lock(hotkeyMutex);
bool foundRecord = false;
for (auto it = sysConflictHotkeyMap.begin(); it != sysConflictHotkeyMap.end();)
{
auto& conflictSet = it->second;
for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
{
if (setIt->moduleName == moduleName)
{
removedHotkeys.push_back(*setIt);
setIt = conflictSet.erase(setIt);
foundRecord = true;
}
else
{
++setIt;
}
}
if (conflictSet.empty())
{
it = sysConflictHotkeyMap.erase(it);
}
else
{
++it;
}
}
for (auto it = inAppConflictHotkeyMap.begin(); it != inAppConflictHotkeyMap.end();)
{
auto& conflictSet = it->second;
uint16_t handle = it->first;
for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
{
if (setIt->moduleName == moduleName)
{
removedHotkeys.push_back(*setIt);
setIt = conflictSet.erase(setIt);
foundRecord = true;
}
else
{
++setIt;
}
}
if (conflictSet.empty())
{
it = inAppConflictHotkeyMap.erase(it);
}
else if (conflictSet.size() == 1)
{
// Move the only remaining conflict to main map
const auto& onlyConflict = *conflictSet.begin();
hotkeyMap[handle] = onlyConflict;
it = inAppConflictHotkeyMap.erase(it);
}
else
{
++it;
}
}
for (auto it = hotkeyMap.begin(); it != hotkeyMap.end();)
{
if (it->second.moduleName == moduleName)
{
uint16_t handle = it->first;
removedHotkeys.push_back(it->second);
it = hotkeyMap.erase(it);
foundRecord = true;
auto inAppIt = inAppConflictHotkeyMap.find(handle);
if (inAppIt != inAppConflictHotkeyMap.end() && inAppIt->second.size() == 1)
{
// Move the only in-app conflict to main map
const auto& onlyConflict = *inAppIt->second.begin();
hotkeyMap[handle] = onlyConflict;
inAppConflictHotkeyMap.erase(inAppIt);
}
}
else
{
++it;
}
}
return removedHotkeys;
}
void HotkeyConflictManager::EnableHotkeyByModule(const std::wstring& moduleName)
{
if (disabledHotkeys.find(moduleName) == disabledHotkeys.end())
{
return; // No disabled hotkeys for this module
}
auto hotkeys = disabledHotkeys[moduleName];
disabledHotkeys.erase(moduleName);
for (const auto& hotkeyInfo : hotkeys)
{
// Re-add the hotkey as enabled
AddHotkey(hotkeyInfo.hotkey, moduleName.c_str(), hotkeyInfo.hotkeyID, true);
}
}
void HotkeyConflictManager::DisableHotkeyByModule(const std::wstring& moduleName)
{
auto hotkeys = RemoveHotkeyByModule(moduleName);
disabledHotkeys[moduleName] = hotkeys;
}
bool HotkeyConflictManager::HasConflictWithSystemHotkey(const Hotkey& hotkey)
{
// Convert PowerToys Hotkey format to Win32 RegisterHotKey format
UINT modifiers = 0;
if (hotkey.win)
{
modifiers |= MOD_WIN;
}
if (hotkey.ctrl)
{
modifiers |= MOD_CONTROL;
}
if (hotkey.alt)
{
modifiers |= MOD_ALT;
}
if (hotkey.shift)
{
modifiers |= MOD_SHIFT;
}
// No modifiers or no key is not a valid hotkey
if (modifiers == 0 || hotkey.key == 0)
{
return false;
}
// Use a unique ID for this test registration
const int hotkeyId = 0x0FFF; // Arbitrary ID for temporary registration
// Try to register the hotkey with Windows, using nullptr instead of a window handle
if (!RegisterHotKey(nullptr, hotkeyId, modifiers, hotkey.key))
{
// If registration fails with ERROR_HOTKEY_ALREADY_REGISTERED, it means the hotkey
// is already in use by the system or another application
if (GetLastError() == ERROR_HOTKEY_ALREADY_REGISTERED)
{
return true;
}
}
else
{
// If registration succeeds, unregister it immediately
UnregisterHotKey(nullptr, hotkeyId);
}
return false;
}
json::JsonObject HotkeyConflictManager::GetHotkeyConflictsAsJson()
{
std::lock_guard<std::mutex> lock(hotkeyMutex);
using namespace json;
JsonObject root;
// Serialize hotkey to a unique string format for grouping
auto serializeHotkey = [](const Hotkey& hotkey) -> JsonObject {
JsonObject obj;
obj.Insert(L"win", value(hotkey.win));
obj.Insert(L"ctrl", value(hotkey.ctrl));
obj.Insert(L"shift", value(hotkey.shift));
obj.Insert(L"alt", value(hotkey.alt));
obj.Insert(L"key", value(static_cast<int>(hotkey.key)));
return obj;
};
// New format: Group conflicts by hotkey
JsonArray inAppConflictsArray;
JsonArray sysConflictsArray;
// Process in-app conflicts - only include hotkeys that are actually in conflict
for (const auto& [handle, conflicts] : inAppConflictHotkeyMap)
{
if (!conflicts.empty())
{
JsonObject conflictGroup;
// All entries have the same hotkey, so use the first one for the key
conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
// Create an array of module info without repeating the hotkey
JsonArray modules;
for (const auto& info : conflicts)
{
JsonObject moduleInfo;
moduleInfo.Insert(L"moduleName", value(info.moduleName));
moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID));
modules.Append(moduleInfo);
}
conflictGroup.Insert(L"modules", modules);
inAppConflictsArray.Append(conflictGroup);
}
}
// Process system conflicts - only include hotkeys that are actually in conflict
for (const auto& [handle, conflicts] : sysConflictHotkeyMap)
{
if (!conflicts.empty())
{
JsonObject conflictGroup;
// All entries have the same hotkey, so use the first one for the key
conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
// Create an array of module info without repeating the hotkey
JsonArray modules;
for (const auto& info : conflicts)
{
JsonObject moduleInfo;
moduleInfo.Insert(L"moduleName", value(info.moduleName));
moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID));
modules.Append(moduleInfo);
}
conflictGroup.Insert(L"modules", modules);
sysConflictsArray.Append(conflictGroup);
}
}
// Add the grouped conflicts to the root object
root.Insert(L"inAppConflicts", inAppConflictsArray);
root.Insert(L"sysConflicts", sysConflictsArray);
return root;
}
uint16_t HotkeyConflictManager::GetHotkeyHandle(const Hotkey& hotkey)
{
uint16_t handle = hotkey.key;
handle |= hotkey.win << 8;
handle |= hotkey.ctrl << 9;
handle |= hotkey.shift << 10;
handle |= hotkey.alt << 11;
return handle;
}
}

View File

@@ -1,100 +0,0 @@
#pragma once
#include "pch.h"
#include <unordered_map>
#include <unordered_set>
#include <string>
#include "../modules/interface/powertoy_module_interface.h"
#include "centralized_hotkeys.h"
#include "common/utils/json.h"
namespace HotkeyConflictDetector
{
using Hotkey = PowertoyModuleIface::Hotkey;
using HotkeyEx = PowertoyModuleIface::HotkeyEx;
using Shortcut = CentralizedHotkeys::Shortcut;
struct HotkeyConflictInfo
{
Hotkey hotkey;
std::wstring moduleName;
int hotkeyID = 0;
inline bool operator==(const HotkeyConflictInfo& other) const
{
return hotkey == other.hotkey &&
moduleName == other.moduleName &&
hotkeyID == other.hotkeyID;
}
};
Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut);
enum HotkeyConflictType
{
NoConflict = 0,
SystemConflict = 1,
InAppConflict = 2,
};
class HotkeyConflictManager
{
public:
static HotkeyConflictManager& GetInstance();
HotkeyConflictType HasConflict(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID);
HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey);
std::vector<HotkeyConflictInfo> HotkeyConflictManager::GetAllConflicts(Hotkey const& hotkey);
bool AddHotkey(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID, bool isEnabled);
std::vector<HotkeyConflictInfo> RemoveHotkeyByModule(const std::wstring& moduleName);
void EnableHotkeyByModule(const std::wstring& moduleName);
void DisableHotkeyByModule(const std::wstring& moduleName);
json::JsonObject GetHotkeyConflictsAsJson();
private:
static std::mutex instanceMutex;
static HotkeyConflictManager* instance;
std::mutex hotkeyMutex;
// Hotkey in hotkeyMap means the hotkey has been registered successfully
std::unordered_map<uint16_t, HotkeyConflictInfo> hotkeyMap;
// Hotkey in sysConflictHotkeyMap means the hotkey has conflict with system defined hotkeys
std::unordered_map<uint16_t, std::unordered_set<HotkeyConflictInfo>> sysConflictHotkeyMap;
// Hotkey in inAppConflictHotkeyMap means the hotkey has conflict with other modules
std::unordered_map<uint16_t, std::unordered_set<HotkeyConflictInfo>> inAppConflictHotkeyMap;
std::unordered_map<std::wstring, std::vector<HotkeyConflictInfo>> disabledHotkeys;
uint16_t GetHotkeyHandle(const Hotkey&);
bool HasConflictWithSystemHotkey(const Hotkey&);
HotkeyConflictManager() = default;
};
};
namespace std
{
template<>
struct hash<HotkeyConflictDetector::HotkeyConflictInfo>
{
size_t operator()(const HotkeyConflictDetector::HotkeyConflictInfo& info) const
{
size_t hotkeyHash =
(info.hotkey.win ? 1ULL : 0ULL) |
((info.hotkey.ctrl ? 1ULL : 0ULL) << 1) |
((info.hotkey.shift ? 1ULL : 0ULL) << 2) |
((info.hotkey.alt ? 1ULL : 0ULL) << 3) |
(static_cast<size_t>(info.hotkey.key) << 4);
size_t moduleHash = std::hash<std::wstring>{}(info.moduleName);
size_t idHash = std::hash<int>{}(info.hotkeyID);
return hotkeyHash ^
((moduleHash << 1) | (moduleHash >> (sizeof(size_t) * 8 - 1))) ^ // rotate left 1 bit
((idHash << 2) | (idHash >> (sizeof(size_t) * 8 - 2))); // rotate left 2 bits
}
};
}

View File

@@ -40,14 +40,13 @@ json::JsonObject PowertoyModule::json_config() const
}
PowertoyModule::PowertoyModule(PowertoyModuleIface* pt_module, HMODULE handle) :
handle(handle), pt_module(pt_module), hkmng(HotkeyConflictDetector::HotkeyConflictManager::GetInstance())
handle(handle), pt_module(pt_module)
{
if (!pt_module)
{
throw std::runtime_error("Module not initialized");
}
remove_hotkey_records();
update_hotkeys();
UpdateHotkeyEx();
}
@@ -64,27 +63,19 @@ void PowertoyModule::update_hotkeys()
for (size_t i = 0; i < hotkeyCount; 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);
});
}
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*/) {
@@ -92,9 +83,6 @@ void PowertoyModule::UpdateHotkeyEx()
modulePtr->OnHotkeyEx();
};
HotkeyConflictDetector::Hotkey _hotkey = HotkeyConflictDetector::ShortcutToHotkey({ hotkey.modifiersMask, hotkey.vkCode });
hkmng.AddHotkey(_hotkey, pt_module->get_key(), 0, pt_module->is_enabled()); // This is the only one activation hotkey, so we use "0" as the name.
CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode }, { pt_module->get_key(), action });
}

View File

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

View File

@@ -15,7 +15,7 @@
#define APPICON 101
#define ID_TRAY_MENU 102
#define ID_CLOSE_MENU_COMMAND 40001
#define ID_EXIT_MENU_COMMAND 40001
#define ID_SETTINGS_MENU_COMMAND 40002
#define ID_ABOUT_MENU_COMMAND 40003
#define ID_REPORT_BUG_COMMAND 40004

Binary file not shown.

View File

@@ -51,7 +51,6 @@
<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>
@@ -72,7 +71,6 @@
<ClInclude Include="bug_report.h" />
<ClInclude Include="centralized_hotkeys.h" />
<ClInclude Include="general_settings.h" />
<ClInclude Include="hotkey_conflict_detector.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="centralized_kb_hook.h" />
<ClInclude Include="settings_telemetry.h" />

View File

@@ -45,9 +45,6 @@
<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" />
@@ -96,9 +93,6 @@
<ClInclude Include="bug_report.h">
<Filter>Utils</Filter>
</ClInclude>
<ClInclude Include="hotkey_conflict_detector.h">
<Filter>Utils</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Utils">

View File

@@ -13,7 +13,6 @@
#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>
@@ -154,8 +153,6 @@ 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();
}
@@ -252,77 +249,6 @@ void dispatch_received_json(const std::wstring& json_to_parse)
const std::wstring save_file_location = PTSettingsHelper::get_root_save_folder_location() + language_filename;
json::to_file(save_file_location, j);
}
else if (name == L"check_hotkey_conflict")
{
try
{
PowertoyModuleIface::Hotkey hotkey;
hotkey.win = value.GetObjectW().GetNamedBoolean(L"win", false);
hotkey.ctrl = value.GetObjectW().GetNamedBoolean(L"ctrl", false);
hotkey.shift = value.GetObjectW().GetNamedBoolean(L"shift", false);
hotkey.alt = value.GetObjectW().GetNamedBoolean(L"alt", false);
hotkey.key = static_cast<unsigned char>(value.GetObjectW().GetNamedNumber(L"key", 0));
std::wstring requestId = value.GetObjectW().GetNamedString(L"request_id", L"").c_str();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
bool hasConflict = hkmng.HasConflict(hotkey);
json::JsonObject response;
response.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"hotkey_conflict_result"));
response.SetNamedValue(L"request_id", json::JsonValue::CreateStringValue(requestId));
response.SetNamedValue(L"has_conflict", json::JsonValue::CreateBooleanValue(hasConflict));
if (hasConflict)
{
auto conflicts = hkmng.GetAllConflicts(hotkey);
if (!conflicts.empty())
{
// Include all conflicts in the response
json::JsonArray allConflicts;
for (const auto& conflict : conflicts)
{
json::JsonObject conflictObj;
conflictObj.SetNamedValue(L"module", json::JsonValue::CreateStringValue(conflict.moduleName));
conflictObj.SetNamedValue(L"hotkeyID", json::JsonValue::CreateNumberValue(conflict.hotkeyID));
allConflicts.Append(conflictObj);
}
response.SetNamedValue(L"all_conflicts", allConflicts);
}
}
std::unique_lock lock{ ipc_mutex };
if (current_settings_ipc)
{
current_settings_ipc->send(response.Stringify().c_str());
}
}
catch (...)
{
Logger::error(L"Failed to process hotkey conflict check request");
}
}
else if (name == L"get_all_hotkey_conflicts")
{
try
{
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
auto conflictsJson = hkmng.GetHotkeyConflictsAsJson();
// Add response type identifier
conflictsJson.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"all_hotkey_conflicts"));
std::unique_lock lock{ ipc_mutex };
if (current_settings_ipc)
{
current_settings_ipc->send(conflictsJson.Stringify().c_str());
}
}
catch (...)
{
Logger::error(L"Failed to process get all hotkey conflicts request");
}
}
}
return;
}

View File

@@ -84,7 +84,7 @@ void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam)
open_settings_window(settings_window, false);
}
break;
case ID_CLOSE_MENU_COMMAND:
case ID_EXIT_MENU_COMMAND:
if (h_menu)
{
DestroyMenu(h_menu);
@@ -191,12 +191,12 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
if (h_menu)
{
static std::wstring settings_menuitem_label = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT);
static std::wstring close_menuitem_label = GET_RESOURCE_STRING(IDS_CLOSE_MENU_TEXT);
static std::wstring exit_menuitem_label = GET_RESOURCE_STRING(IDS_EXIT_MENU_TEXT);
static std::wstring submit_bug_menuitem_label = GET_RESOURCE_STRING(IDS_SUBMIT_BUG_TEXT);
static std::wstring documentation_menuitem_label = GET_RESOURCE_STRING(IDS_DOCUMENTATION_MENU_TEXT);
static std::wstring quick_access_menuitem_label = GET_RESOURCE_STRING(IDS_QUICK_ACCESS_MENU_TEXT);
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
change_menu_item_text(ID_CLOSE_MENU_COMMAND, close_menuitem_label.data());
change_menu_item_text(ID_EXIT_MENU_COMMAND, exit_menuitem_label.data());
change_menu_item_text(ID_REPORT_BUG_COMMAND, submit_bug_menuitem_label.data());
bool bug_report_disabled = is_bug_report_running();
EnableMenuItem(h_sub_menu, ID_REPORT_BUG_COMMAND, MF_BYCOMMAND | (bug_report_disabled ? MF_GRAYED : MF_ENABLED));

View File

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

View File

@@ -28,23 +28,16 @@ public sealed class AdvancedPasteAdditionalActions
public IEnumerable<IAdvancedPasteAction> GetAllActions()
{
return GetAllActionsRecursive([ImageToText, PasteAsFile, Transcode]);
}
Queue<IAdvancedPasteAction> queue = new([ImageToText, PasteAsFile, Transcode]);
/// <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)
while (queue.Count != 0)
{
var action = queue.Dequeue();
yield return action;
foreach (var subAction in GetAllActionsRecursive(action.SubActions))
foreach (var subAction in action.SubActions)
{
yield return subAction;
queue.Enqueue(subAction);
}
}
}

View File

@@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Library;
@@ -20,8 +20,6 @@ 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
@@ -67,6 +65,7 @@ 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();
}
}
@@ -100,20 +99,6 @@ 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 => [];
@@ -133,8 +118,6 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
IsShown = other.IsShown;
CanMoveUp = other.CanMoveUp;
CanMoveDown = other.CanMoveDown;
HasConflict = other.HasConflict;
Tooltip = other.Tooltip;
}
private HotkeySettings GetShortcutClone()

View File

@@ -3,16 +3,14 @@
// 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, IHotkeyConfig
public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig
{
public const string ModuleName = "AdvancedPaste";
@@ -41,64 +39,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName);
}
public ModuleType GetModuleType() => ModuleType.AdvancedPaste;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.PasteAsPlainTextShortcut,
value => Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut,
"PasteAsPlainText_Shortcut"),
new HotkeyAccessor(
() => Properties.AdvancedPasteUIShortcut,
value => Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut,
"AdvancedPasteUI_Shortcut"),
new HotkeyAccessor(
() => Properties.PasteAsMarkdownShortcut,
value => Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings(),
"PasteAsMarkdown_Shortcut"),
new HotkeyAccessor(
() => Properties.PasteAsJsonShortcut,
value => Properties.PasteAsJsonShortcut = value ?? new HotkeySettings(),
"PasteAsJson_Shortcut"),
};
string[] additionalActionHeaderKeys =
[
"ImageToText",
"PasteAsTxtFile",
"PasteAsPngFile",
"PasteAsHtmlFile",
"TranscodeToMp3",
"TranscodeToMp4",
];
int index = 0;
foreach (var action in Properties.AdditionalActions.GetAllActions())
{
if (action is AdvancedPasteAdditionalAction additionalAction)
{
hotkeyAccessors.Add(new HotkeyAccessor(
() => additionalAction.Shortcut,
value => additionalAction.Shortcut = value ?? new HotkeySettings(),
additionalActionHeaderKeys[index]));
index++;
}
}
// Custom actions do not have localization header, just use the action name.
foreach (var customAction in Properties.CustomActions.Value)
{
hotkeyAccessors.Add(new HotkeyAccessor(
() => customAction.Shortcut,
value => customAction.Shortcut = value ?? new HotkeySettings(),
customAction.Name));
}
return hotkeyAccessors.ToArray();
}
public string GetModuleName()
=> Name;

View File

@@ -2,15 +2,13 @@
// 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, IHotkeyConfig
public class AlwaysOnTopSettings : BasePTModuleSettings, ISettingsConfig
{
public const string ModuleName = "AlwaysOnTop";
public const string ModuleVersion = "0.0.1";
@@ -30,21 +28,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
public ModuleType GetModuleType() => ModuleType.AlwaysOnTop;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.Hotkey.Value,
value => Properties.Hotkey.Value = value ?? AlwaysOnTopProperties.DefaultHotkeyValue,
"AlwaysOnTop_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
public bool UpgradeSettingsConfiguration()
{
return false;

View File

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

View File

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

View File

@@ -2,15 +2,13 @@
// 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, IHotkeyConfig
public class CropAndLockSettings : BasePTModuleSettings, ISettingsConfig
{
public const string ModuleName = "CropAndLock";
public const string ModuleVersion = "0.0.1";
@@ -30,25 +28,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
public ModuleType GetModuleType() => ModuleType.CropAndLock;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ReparentHotkey.Value,
value => Properties.ReparentHotkey.Value = value ?? CropAndLockProperties.DefaultReparentHotkeyValue,
"CropAndLock_ReparentActivation_Shortcut"),
new HotkeyAccessor(
() => Properties.ThumbnailHotkey.Value,
value => Properties.ThumbnailHotkey.Value = value ?? CropAndLockProperties.DefaultThumbnailHotkeyValue,
"CropAndLock_ThumbnailActivation_Shortcut"),
};
return hotkeyAccessors.ToArray();
}
public bool UpgradeSettingsConfiguration()
{
return false;

View File

@@ -2,15 +2,13 @@
// 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, IHotkeyConfig
public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig
{
public const string ModuleName = "FindMyMouse";
@@ -29,21 +27,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
public ModuleType GetModuleType() => ModuleType.FindMyMouse;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.ActivationShortcut,
value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
"MouseUtils_FindMyMouse_ActivationShortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{

View File

@@ -1,34 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
{
public class HotkeyAccessor
{
public Func<HotkeySettings> Getter { get; }
public Action<HotkeySettings> Setter { get; }
public HotkeyAccessor(Func<HotkeySettings> getter, Action<HotkeySettings> setter, string localizationHeaderKey = "")
{
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter ?? throw new ArgumentNullException(nameof(setter));
LocalizationHeaderKey = localizationHeaderKey;
}
public HotkeySettings Value
{
get => Getter();
set => Setter(value);
}
public string LocalizationHeaderKey { get; set; }
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class AllHotkeyConflictsData
{
public List<HotkeyConflictGroupData> InAppConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public List<HotkeyConflictGroupData> SystemConflicts { get; set; } = new List<HotkeyConflictGroupData>();
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class AllHotkeyConflictsEventArgs : EventArgs
{
public AllHotkeyConflictsData Conflicts { get; }
public AllHotkeyConflictsEventArgs(AllHotkeyConflictsData conflicts)
{
Conflicts = conflicts;
}
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class HotkeyConflictGroupData
{
public HotkeyData Hotkey { get; set; }
public bool IsSystemConflict { get; set; }
public List<ModuleHotkeyData> Modules { get; set; }
}
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class HotkeyConflictInfo
{
public bool IsSystemConflict { get; set; }
public string ConflictingModuleName { get; set; }
public int ConflictingHotkeyID { get; set; }
public List<string> AllConflictingModules { get; set; } = new List<string>();
}
}

View File

@@ -1,71 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class HotkeyData
{
public bool Win { get; set; }
public bool Ctrl { get; set; }
public bool Shift { get; set; }
public bool Alt { get; set; }
public int Key { get; set; }
public List<object> GetKeysList()
{
List<object> shortcutList = new List<object>();
if (Win)
{
shortcutList.Add(92); // The Windows key or button.
}
if (Ctrl)
{
shortcutList.Add("Ctrl");
}
if (Alt)
{
shortcutList.Add("Alt");
}
if (Shift)
{
shortcutList.Add(16); // The Shift key or button.
}
if (Key > 0)
{
switch (Key)
{
// https://learn.microsoft.com/uwp/api/windows.system.virtualkey?view=winrt-20348
case 38: // The Up Arrow key or button.
case 40: // The Down Arrow key or button.
case 37: // The Left Arrow key or button.
case 39: // The Right Arrow key or button.
shortcutList.Add(Key);
break;
default:
var localKey = Helper.GetKeyName((uint)Key);
shortcutList.Add(localKey);
break;
}
}
return shortcutList;
}
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class ModuleConflictsData
{
public List<HotkeyConflictGroupData> InAppConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public List<HotkeyConflictGroupData> SystemConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public bool HasConflicts => InAppConflicts.Count > 0 || SystemConflicts.Count > 0;
}
}

View File

@@ -1,84 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Windows.Web.AtomPub;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class ModuleHotkeyData : INotifyPropertyChanged
{
private string _moduleName;
private int _hotkeyID;
private HotkeySettings _hotkeySettings;
private bool _isSystemConflict;
public event PropertyChangedEventHandler PropertyChanged;
public string IconPath { get; set; }
public string DisplayName { get; set; }
public string Header { get; set; }
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string ModuleName
{
get => _moduleName;
set
{
if (_moduleName != value)
{
_moduleName = value;
}
}
}
public int HotkeyID
{
get => _hotkeyID;
set
{
if (_hotkeyID != value)
{
_hotkeyID = value;
}
}
}
public HotkeySettings HotkeySettings
{
get => _hotkeySettings;
set
{
if (_hotkeySettings != value)
{
_hotkeySettings = value;
OnPropertyChanged();
}
}
}
public bool IsSystemConflict
{
get => _isSystemConflict;
set
{
if (_isSystemConflict != value)
{
_isSystemConflict = value;
}
}
}
public ModuleType ModuleType { get; set; }
}
}

View File

@@ -4,29 +4,17 @@
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, INotifyPropertyChanged
public record HotkeySettings : ICmdLineRepresentable
{
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()
{
@@ -35,8 +23,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = false;
Shift = false;
Code = 0;
HasConflict = false;
}
/// <summary>
@@ -54,51 +40,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = alt;
Shift = shift;
Code = code;
HasConflict = false;
}
public bool HasConflict
{
get => _hasConflict;
set
{
if (_hasConflict != value)
{
_hasConflict = value;
OnPropertyChanged();
}
}
}
public string ConflictDescription
{
get => _conflictDescription ?? string.Empty;
set
{
if (_conflictDescription != value)
{
_conflictDescription = value;
OnPropertyChanged();
}
}
}
public bool IsSystemConflict
{
get => _isSystemConflict;
set
{
if (_isSystemConflict != value)
{
_isSystemConflict = value;
OnPropertyChanged();
}
}
}
public virtual void UpdateConflictStatus()
{
Logger.LogInfo($"{this.ToString()}");
}
[JsonPropertyName("win")]

View File

@@ -1,17 +0,0 @@
// 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();
}
}

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