diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 7e460dba2f..004d9f4269 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -25,11 +25,14 @@ ADMINS
adml
admx
advancedpaste
+advancedpasteui
+advancedpasteuishortcut
advfirewall
AFeature
affordances
AFX
AGGREGATABLE
+AHK
AHybrid
akv
ALarger
@@ -40,6 +43,7 @@ ALLINPUT
Allman
Allmodule
ALLOWUNDO
+allpc
ALLVIEW
ALPHATYPE
AModifier
@@ -629,6 +633,7 @@ HKCU
hkey
HKLM
HKM
+hkmng
HKPD
HKU
HMD
@@ -646,7 +651,11 @@ Hostx
hotfixes
hotkeycontrol
HOTKEYF
+hotkeylockmachine
+hotkeyreconnect
hotkeys
+hotkeyswitch
+hotkeytoggleeasymouse
hotlight
hotspot
HPAINTBUFFER
@@ -659,6 +668,7 @@ HROW
hsb
HSCROLL
hsi
+HSpeed
HTCLIENT
hthumbnail
HTOUCHINPUT
@@ -704,9 +714,12 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
+imagetotext
+imagetotextshortcut
imagingdevices
ime
imgflip
+inapp
inbox
INCONTACT
Indo
@@ -760,6 +773,7 @@ istep
ith
ITHUMBNAIL
IUI
+IUWP
IWIC
jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
@@ -789,6 +803,7 @@ keyvault
KILLFOCUS
killrunner
kmph
+kvp
Kybd
lastcodeanalysissucceeded
LASTEXITCODE
@@ -827,6 +842,7 @@ localappdata
localpackage
LOCALSYSTEM
LOCATIONCHANGE
+LOCKMACHINE
LOCKTYPE
LOGFONT
LOGFONTW
@@ -912,6 +928,7 @@ MDL
mdtext
mdtxt
mdwn
+measuretool
meme
memicmp
MENUITEMINFO
@@ -961,6 +978,7 @@ MOUSEHWHEEL
MOUSEINPUT
mousejump
mousepointer
+mousepointercrosshairs
mouseutils
MOVESIZEEND
MOVESIZESTART
@@ -1100,6 +1118,7 @@ NOTSRCCOPY
NOTSRCERASE
notwindows
NOTXORPEN
+nowarn
NOZORDER
NPH
npmjs
@@ -1161,6 +1180,18 @@ PARENTRELATIVEFORADDRESSBAR
PARENTRELATIVEPARSING
parray
PARTIALCONFIRMATIONDIALOGTITLE
+pasteashtmlfile
+pasteashtmlfileshortcut
+pasteasjson
+pasteasjsonshortcut
+pasteasmarkdown
+pasteasmarkdownshortcut
+pasteasplaintext
+pasteasplaintextshortcut
+pasteaspngfile
+pasteaspngfileshortcut
+pasteastxtfile
+pasteastxtfileshortcut
PATCOPY
PATHMUSTEXIST
PATINVERT
@@ -1228,6 +1259,7 @@ Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
+powerocr
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1368,6 +1400,7 @@ Removelnk
renamable
RENAMEONCOLLISION
reparented
+reparenthotkey
reparenting
reportfileaccesses
requery
@@ -1617,6 +1650,7 @@ STYLECHANGED
STYLECHANGING
subkeys
sublang
+Subdomain
SUBMODULEUPDATE
subresource
Superbar
@@ -1687,6 +1721,7 @@ THH
THICKFRAME
THISCOMPONENT
throughs
+thumbnailhotkey
TILEDWINDOW
TILLSON
timedate
@@ -1701,6 +1736,7 @@ tlb
tlbimp
tlc
TNP
+TOGGLEEASYMOUSE
Toolhelp
toolkitconverters
toolwindow
@@ -1714,6 +1750,7 @@ tracelogging
tracerpt
trackbar
trafficmanager
+transcodetomp
transicc
TRAYMOUSEMESSAGE
triaging
@@ -1828,6 +1865,7 @@ VSINSTALLDIR
VSM
vso
vsonline
+VSpeed
vstemplate
vstest
VSTHRD
@@ -1964,10 +2002,13 @@ XNamespace
Xoshiro
XPels
XPixel
+XPos
XResource
xsi
+XSpeed
XStr
xstyler
+XTimer
XUP
XVIRTUALSCREEN
xxxxxx
@@ -1977,7 +2018,10 @@ YIncrement
yinle
yinyue
YPels
+YPos
YResolution
+YSpeed
+YTimer
YStr
YVIRTUALSCREEN
ZEROINIT
diff --git a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
index e3120836c8..af9ab8ff6f 100644
--- a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
+++ b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
@@ -57,12 +57,19 @@ $totalList = $projFiles | ForEach-Object -Parallel {
$p = -split $p
$p = $p[1, 2]
- $tempString = $p[0] + " " + $p[1]
+ $tempString = $p[0]
- if(![string]::IsNullOrWhiteSpace($tempString))
+ if([string]::IsNullOrWhiteSpace($tempString))
{
- echo "- $tempString";
+ Continue
}
+
+ if($tempString.StartsWith("Microsoft.") -Or $tempString.StartsWith("System."))
+ {
+ Continue
+ }
+
+ echo "- $tempString"
}
$csproj = $null;
}
diff --git a/.pipelines/verifyNugetPackages.ps1 b/.pipelines/verifyNugetPackages.ps1
index 54d0137121..c7cebbd383 100644
--- a/.pipelines/verifyNugetPackages.ps1
+++ b/.pipelines/verifyNugetPackages.ps1
@@ -21,4 +21,13 @@ 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
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8e7a47bf95..47546d9337 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -65,7 +65,8 @@
-
+
+
@@ -103,7 +104,7 @@
-
+
@@ -113,4 +114,4 @@
-
\ No newline at end of file
+
diff --git a/NOTICE.md b/NOTICE.md
index 4e9ae35711..bedc11379d 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1491,93 +1491,50 @@ SOFTWARE.
## NuGet Packages used by PowerToys
-- 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 10.0.0-preview.7.25380.108
-- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0
-- Microsoft.CodeAnalysis.NetAnalyzers 10.0.0-preview.25380.108
-- Microsoft.Data.Sqlite 10.0.0-preview.7.25380.108
-- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
-- Microsoft.DotNet.ILCompiler (A)
-- Microsoft.Extensions.DependencyInjection 10.0.0-preview.7.25380.108
-- Microsoft.Extensions.Hosting 10.0.0-preview.7.25380.108
-- Microsoft.Extensions.Hosting.WindowsServices 10.0.0-preview.7.25380.108
-- Microsoft.Extensions.Logging 10.0.0-preview.7.25380.108
-- Microsoft.Extensions.Logging.Abstractions 10.0.0-preview.7.25380.108
-- 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 10.0.0-preview.7.25380.108
-- Microsoft.Windows.Compatibility 10.0.0-preview.7.25380.108
-- 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 10.0.0-preview.7.25380.108
-- System.CommandLine 2.0.0-beta4.22272.1
-- System.ComponentModel.Composition 10.0.0-preview.7.25380.108
-- System.Configuration.ConfigurationManager 10.0.0-preview.7.25380.108
-- System.Data.OleDb 10.0.0-preview.7.25380.108
-- System.Data.SqlClient 4.9.0
-- System.Diagnostics.EventLog 10.0.0-preview.7.25380.108
-- System.Diagnostics.PerformanceCounter 10.0.0-preview.7.25380.108
-- System.Drawing.Common 10.0.0-preview.7.25380.108
-- System.IO.Abstractions 22.0.13
-- System.IO.Abstractions.TestingHelpers 22.0.13
-- System.Management 10.0.0-preview.7.25380.108
-- System.Net.Http 4.3.4
-- System.Private.Uri 4.3.2
-- System.Reactive 6.0.1
-- System.Runtime.Caching 10.0.0-preview.7.25380.108
-- System.ServiceProcess.ServiceController 10.0.0-preview.7.25380.108
-- System.Text.Encoding.CodePages 10.0.0-preview.7.25380.108
-- System.Text.Json 10.0.0-preview.7.25380.108
-- 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
+- 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
\ No newline at end of file
diff --git a/PowerToys.sln b/PowerToys.sln
index 00986aae29..2b6d42305f 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -262,6 +262,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
src\common\utils\EventLocker.h = src\common\utils\EventLocker.h
src\common\utils\EventWaiter.h = src\common\utils\EventWaiter.h
src\common\utils\excluded_apps.h = src\common\utils\excluded_apps.h
+ src\common\utils\shell_ext_registration.h = src\common\utils\shell_ext_registration.h
src\common\utils\exec.h = src\common\utils\exec.h
src\common\utils\game_mode.h = src\common\utils\game_mode.h
src\common\utils\gpo.h = src\common\utils\gpo.h
@@ -788,6 +789,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
EndProject
+Project("{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
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.WebSearch.UnitTests\Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj", "{E816D7B2-4688-4ECB-97CC-3D8E798F3831}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2850,6 +2859,38 @@ 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
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|ARM64.Build.0 = Debug|ARM64
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|x64.ActiveCfg = Debug|x64
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|x64.Build.0 = Debug|x64
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|ARM64.ActiveCfg = Release|ARM64
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|ARM64.Build.0 = Release|ARM64
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|x64.ActiveCfg = Release|x64
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|x64.Build.0 = Release|x64
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|ARM64.Build.0 = Debug|ARM64
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|x64.ActiveCfg = Debug|x64
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|x64.Build.0 = Debug|x64
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.ActiveCfg = Release|ARM64
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3161,6 +3202,10 @@ 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}
+ {E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
+ {E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/doc/devdocs/core/settings/settings-implementation.md b/doc/devdocs/core/settings/settings-implementation.md
index d97aff2dac..defe59a3fa 100644
--- a/doc/devdocs/core/settings/settings-implementation.md
+++ b/doc/devdocs/core/settings/settings-implementation.md
@@ -71,6 +71,41 @@ When the user changes settings in the UI:
3. The runner calls the `set_config` function on the appropriate module
4. The module parses the JSON and applies the new settings
+# Shortcut Conflict Detection
+
+Steps to enable conflict detection for a hotkey:
+
+### 1. Implement module interface for hotkeys
+Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional GetHotkeyEx()`.
+
+- If not yet implemented, you need to add it so that it returns all hotkeys used by the module.
+- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup.
+- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp`
+
+### 2. Implement IHotkeyConfig in the module settings (UI side)
+Make sure the module’s settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`.
+
+- This method should return all hotkeys used in the module.
+- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`).
+- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs`
+- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings.
+It provides both `getter` and `setter` methods to read and update the corresponding hotkey.
+Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey.
+This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`
+
+### 3. Update the module’s ViewModel
+The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary GetAllHotkeySettings()`.
+
+- This method should return all hotkeys, maintaining the same order as in steps 1 and 2.
+- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs`
+
+### 4. Ensure the module’s Views call `OnPageLoaded()`
+Once the module’s view is loaded, make sure to invoke the ViewModel’s `OnPageLoaded()` method:
+```cs
+Loaded += (s, e) => ViewModel.OnPageLoaded();
+```
+- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
+
## Debugging Settings
To debug settings issues:
diff --git a/doc/images/icons/Mouse Crosshairs.png b/doc/images/icons/Mouse Crosshairs.png
index 6b1dcb9c16..a2c64a72a4 100644
Binary files a/doc/images/icons/Mouse Crosshairs.png and b/doc/images/icons/Mouse Crosshairs.png differ
diff --git a/installer/PowerToysSetup/FileLocksmith.wxs b/installer/PowerToysSetup/FileLocksmith.wxs
index 085e60eaa7..5943ab4147 100644
--- a/installer/PowerToysSetup/FileLocksmith.wxs
+++ b/installer/PowerToysSetup/FileLocksmith.wxs
@@ -14,21 +14,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -38,7 +23,6 @@
-
diff --git a/installer/PowerToysSetup/ImageResizer.wxs b/installer/PowerToysSetup/ImageResizer.wxs
index 17da272014..67b5acf198 100644
--- a/installer/PowerToysSetup/ImageResizer.wxs
+++ b/installer/PowerToysSetup/ImageResizer.wxs
@@ -16,71 +16,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -90,7 +25,6 @@
-
diff --git a/installer/PowerToysSetup/NewPlus.wxs b/installer/PowerToysSetup/NewPlus.wxs
index 4dd1c67701..624c01fca2 100644
--- a/installer/PowerToysSetup/NewPlus.wxs
+++ b/installer/PowerToysSetup/NewPlus.wxs
@@ -18,19 +18,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -40,8 +27,7 @@
-
-
+
@@ -81,7 +67,7 @@
-
+
diff --git a/installer/PowerToysSetup/PowerRename.wxs b/installer/PowerToysSetup/PowerRename.wxs
index 1e722d9334..7aa357e207 100644
--- a/installer/PowerToysSetup/PowerRename.wxs
+++ b/installer/PowerToysSetup/PowerRename.wxs
@@ -14,22 +14,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -39,7 +23,6 @@
-
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index f15b8a4714..77ffad8483 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -176,6 +176,18 @@
Installed AND (REMOVE="ALL")
+
+ Installed AND (REMOVE="ALL")
+
+
+ Installed AND (REMOVE="ALL")
+
+
+ Installed AND (REMOVE="ALL")
+
+
+ Installed AND (REMOVE="ALL")
+
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
@@ -437,6 +449,35 @@
Execute="deferred"
BinaryKey="PTCustomActions"
DllEntry="UnRegisterContextMenuPackagesCA"
+ />
+
+
+
+
+
+#include
+#include
+#include
+
+#include "../logger/logger.h"
+
+namespace runtime_shell_ext
+{
+ struct Spec
+ {
+ // Mandatory
+ std::wstring clsid; // e.g. {GUID}
+ std::wstring sentinelKey; // e.g. Software\\Microsoft\\PowerToys\\ModuleName
+ std::wstring sentinelValue; // e.g. ContextMenuRegistered
+ std::vector dllFileCandidates; // relative filenames (pick first existing)
+ std::vector contextMenuHandlerKeyPaths; // full HKCU relative paths where default value = CLSID
+
+ // Optional
+ std::wstring friendlyName; // if non-empty written as default under CLSID root
+ bool writeOptInEmptyValue = true; // write ContextMenuOptIn="" under CLSID root (legacy pattern)
+ bool writeThreadingModel = true; // write Apartment threading model
+ std::vector extraAssociationPaths; // additional key paths (DragDropHandlers etc.) default=CLSID
+ std::vector systemFileAssocExtensions; // e.g. .png -> Software\\Classes\\SystemFileAssociations\\.png\\ShellEx\\ContextMenuHandlers\\
+ std::wstring systemFileAssocHandlerName; // e.g. ImageResizer
+ std::wstring representativeSystemExt; // used to decide if associations need repair (.png)
+ bool logRepairs = true;
+ };
+
+ namespace detail
+ {
+ // Minimal RAII wrapper for HKEY
+ struct unique_hkey
+ {
+ HKEY h{ nullptr };
+ unique_hkey() = default;
+ explicit unique_hkey(HKEY handle) : h(handle) {}
+ ~unique_hkey() { if (h) RegCloseKey(h); }
+ unique_hkey(const unique_hkey&) = delete;
+ unique_hkey& operator=(const unique_hkey&) = delete;
+ unique_hkey(unique_hkey&& other) noexcept : h(other.h) { other.h = nullptr; }
+ unique_hkey& operator=(unique_hkey&& other) noexcept { if (this != &other) { if (h) RegCloseKey(h); h = other.h; other.h = nullptr; } return *this; }
+ HKEY get() const { return h; }
+ HKEY* put() { if (h) { RegCloseKey(h); h = nullptr; } return &h; }
+ };
+ inline std::wstring base_dir_from_module(HMODULE h)
+ {
+ wchar_t buf[MAX_PATH];
+ if (GetModuleFileNameW(h, buf, MAX_PATH))
+ {
+ PathRemoveFileSpecW(buf);
+ return buf;
+ }
+ return L"";
+ }
+
+ inline std::wstring pick_existing_dll(const std::wstring& base, const std::vector& candidates)
+ {
+ for (const auto& rel : candidates)
+ {
+ std::wstring full = base + L"\\" + rel;
+ if (GetFileAttributesW(full.c_str()) != INVALID_FILE_ATTRIBUTES)
+ {
+ return full;
+ }
+ }
+ if (!candidates.empty())
+ {
+ return base + L"\\" + candidates.front();
+ }
+ return L"";
+ }
+
+ inline bool sentinel_exists(const Spec& spec)
+ {
+ unique_hkey key;
+ if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
+ return false;
+ DWORD v = 0; DWORD sz = sizeof(v);
+ return RegQueryValueExW(key.get(), spec.sentinelValue.c_str(), nullptr, nullptr, reinterpret_cast(&v), &sz) == ERROR_SUCCESS && v == 1;
+ }
+
+ inline void write_sentinel(const Spec& spec)
+ {
+ unique_hkey key;
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
+ {
+ DWORD one = 1;
+ RegSetValueExW(key.get(), spec.sentinelValue.c_str(), 0, REG_DWORD, reinterpret_cast(&one), sizeof(one));
+ }
+ }
+
+ inline void write_inproc_server(const Spec& spec, const std::wstring& dllPath)
+ {
+ using namespace std::string_literals;
+ std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
+ std::wstring inprocKey = clsidRoot + L"\\InprocServer32";
+ {
+ unique_hkey key;
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, clsidRoot.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
+ {
+ if (!spec.friendlyName.empty())
+ {
+ RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast(spec.friendlyName.c_str()), static_cast((spec.friendlyName.size() + 1) * sizeof(wchar_t)));
+ }
+ if (spec.writeOptInEmptyValue)
+ {
+ const wchar_t* optIn = L"ContextMenuOptIn";
+ const wchar_t empty = L'\0';
+ RegSetValueExW(key.get(), optIn, 0, REG_SZ, reinterpret_cast(&empty), sizeof(empty));
+ }
+ }
+ }
+ unique_hkey key;
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
+ {
+ RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast(dllPath.c_str()), static_cast((dllPath.size() + 1) * sizeof(wchar_t)));
+ if (spec.writeThreadingModel)
+ {
+ const wchar_t* tm = L"Apartment";
+ RegSetValueExW(key.get(), L"ThreadingModel", 0, REG_SZ, reinterpret_cast(tm), static_cast((wcslen(tm) + 1) * sizeof(wchar_t)));
+ }
+ }
+ }
+
+ inline std::wstring read_inproc_server(const Spec& spec)
+ {
+ using namespace std::string_literals;
+ std::wstring inprocKey = L"Software\\Classes\\CLSID\\"s + spec.clsid + L"\\InprocServer32";
+ unique_hkey key;
+ if (RegOpenKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
+ return L"";
+ wchar_t buf[MAX_PATH]; DWORD sz = sizeof(buf);
+ if (RegQueryValueExW(key.get(), nullptr, nullptr, nullptr, reinterpret_cast(buf), &sz) == ERROR_SUCCESS)
+ return std::wstring(buf);
+ return L"";
+ }
+
+ inline void write_default_value_key(const std::wstring& keyPath, const std::wstring& value)
+ {
+ unique_hkey key;
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
+ {
+ RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast(value.c_str()), static_cast((value.size() + 1) * sizeof(wchar_t)));
+ }
+ }
+
+ inline bool representative_association_exists(const Spec& spec)
+ {
+ using namespace std::string_literals;
+ if (spec.representativeSystemExt.empty() || spec.systemFileAssocHandlerName.empty())
+ return true;
+ std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + spec.representativeSystemExt + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
+ unique_hkey key;
+ return RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, KEY_READ, key.put()) == ERROR_SUCCESS;
+ }
+ }
+
+ inline bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance)
+ {
+ using namespace std::string_literals;
+ auto base = detail::base_dir_from_module(moduleInstance);
+ auto dllPath = detail::pick_existing_dll(base, spec.dllFileCandidates);
+ if (dllPath.empty())
+ {
+ Logger::error(L"Runtime registration: cannot locate dll path for CLSID {}", spec.clsid);
+ return false;
+ }
+ bool exists = detail::sentinel_exists(spec);
+ bool repaired = false;
+ if (exists)
+ {
+ auto current = detail::read_inproc_server(spec);
+ if (_wcsicmp(current.c_str(), dllPath.c_str()) != 0)
+ {
+ detail::write_inproc_server(spec, dllPath);
+ repaired = true;
+ }
+ if (!detail::representative_association_exists(spec))
+ {
+ repaired = true;
+ }
+ }
+ if (!exists)
+ {
+ detail::write_inproc_server(spec, dllPath);
+ }
+ if (!exists || repaired)
+ {
+ for (const auto& path : spec.contextMenuHandlerKeyPaths)
+ {
+ detail::write_default_value_key(path, spec.clsid);
+ }
+ for (const auto& path : spec.extraAssociationPaths)
+ {
+ detail::write_default_value_key(path, spec.clsid);
+ }
+ if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
+ {
+ for (const auto& ext : spec.systemFileAssocExtensions)
+ {
+ std::wstring path = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
+ detail::write_default_value_key(path, spec.clsid);
+ }
+ }
+ }
+ if (!exists)
+ {
+ detail::write_sentinel(spec);
+ Logger::info(L"Runtime registration completed for CLSID {}", spec.clsid);
+ }
+ else if (repaired && spec.logRepairs)
+ {
+ Logger::info(L"Runtime registration repaired for CLSID {}", spec.clsid);
+ }
+ return true;
+ }
+
+ inline bool Unregister(const Spec& spec)
+ {
+ using namespace std::string_literals;
+ // Remove handler key paths
+ for (const auto& path : spec.contextMenuHandlerKeyPaths)
+ {
+ RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
+ }
+ // Remove extra association paths (e.g., drag & drop handlers)
+ for (const auto& path : spec.extraAssociationPaths)
+ {
+ RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
+ }
+ // Remove per-extension system file association handler keys
+ if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
+ {
+ for (const auto& ext : spec.systemFileAssocExtensions)
+ {
+ std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
+ RegDeleteTreeW(HKEY_CURRENT_USER, keyPath.c_str());
+ }
+ }
+ // Remove CLSID branch
+ if (!spec.clsid.empty())
+ {
+ std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
+ RegDeleteTreeW(HKEY_CURRENT_USER, clsidRoot.c_str());
+ }
+ // Remove sentinel value (not deleting entire key to avoid disturbing other values)
+ if (!spec.sentinelKey.empty() && !spec.sentinelValue.empty())
+ {
+ HKEY hKey{};
+ if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS)
+ {
+ RegDeleteValueW(hKey, spec.sentinelValue.c_str());
+ RegCloseKey(hKey);
+ }
+ }
+ Logger::info(L"Successfully unregistered CLSID {}", spec.clsid);
+ return true;
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
index 896b362735..6af0d636ac 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
@@ -112,7 +112,7 @@ private:
return {};
}
- static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
+ static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true)
{
try
{
@@ -122,6 +122,7 @@ private:
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
+ hotkey.isShown = isShown;
return hotkey;
}
catch (...)
@@ -231,8 +232,10 @@ private:
return false;
}
- void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue)
+ void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true)
{
+ bool actionIsShown = true;
+
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{
return;
@@ -240,9 +243,9 @@ private:
const auto action = actionValue.GetObjectW();
- if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
+ if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown)
{
- return;
+ actionIsShown = false;
}
if (action.HasKey(JSON_KEY_SHORTCUT))
@@ -250,7 +253,7 @@ private:
const AdditionalAction additionalAction
{
actionName.c_str(),
- parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
+ parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
};
m_additional_actions.push_back(additionalAction);
@@ -259,12 +262,12 @@ private:
{
for (const auto& [subActionName, subAction] : action)
{
- process_additional_action(subActionName, subAction);
+ process_additional_action(subActionName, subAction, actionIsShown);
}
}
}
- void read_settings(PowerToysSettings::PowerToyValues& settings)
+ void read_settings(PowerToysSettings::PowerToyValues& settings)
{
const auto settingsObject = settings.get_raw_json();
@@ -317,9 +320,21 @@ private:
{
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
- for (const auto& [actionName, additionalAction] : additionalActions)
+ // Define the expected order to ensure consistent hotkey ID assignment
+ const std::vector expectedOrder = {
+ L"image-to-text",
+ L"paste-as-file",
+ L"transcode"
+ };
+
+ // Process actions in the predefined order
+ for (auto& actionKey : expectedOrder)
{
- process_additional_action(actionName, additionalAction);
+ if (additionalActions.HasKey(actionKey))
+ {
+ const auto actionValue = additionalActions.GetNamedValue(actionKey);
+ process_additional_action(actionKey, actionValue);
+ }
}
}
@@ -331,17 +346,14 @@ private:
for (const auto& customAction : customActions)
{
const auto object = customAction.GetObjectW();
+ bool actionIsShown = object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false);
- if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
- {
- const CustomAction customActionData
- {
- static_cast(object.GetNamedNumber(JSON_KEY_ID)),
- parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
- };
+ const CustomAction customActionData{
+ static_cast(object.GetNamedNumber(JSON_KEY_ID)),
+ parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
+ };
- m_custom_actions.push_back(customActionData);
- }
+ m_custom_actions.push_back(customActionData);
}
}
}
diff --git a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj
index 0c285a8bfa..c67119808f 100644
--- a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj
+++ b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj
@@ -73,6 +73,7 @@
+
diff --git a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj.filters b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj.filters
index c3b4f47ebc..49bf0e21a9 100644
--- a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj.filters
+++ b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj.filters
@@ -27,6 +27,9 @@
Header Files
+
+ Header Files
+
diff --git a/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp b/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp
index ec755d99a3..7d188a2010 100644
--- a/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp
+++ b/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp
@@ -12,6 +12,7 @@
#include "FileLocksmithLib/Constants.h"
#include "FileLocksmithLib/Settings.h"
#include "FileLocksmithLib/Trace.h"
+#include "RuntimeRegistration.h"
#include "dllmain.h"
#include "Generated Files/resource.h"
@@ -82,12 +83,17 @@ public:
{
std::wstring path = get_module_folderpath(globals::instance);
std::wstring packageUri = path + L"\\FileLocksmithContextMenuPackage.msix";
-
if (!package::IsPackageRegisteredWithPowerToysVersion(constants::nonlocalizable::ContextMenuPackageName))
{
package::RegisterSparsePackage(path, packageUri);
}
}
+ else
+ {
+#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
+ FileLocksmithRuntimeRegistration::EnsureRegistered();
+#endif
+ }
m_enabled = true;
}
@@ -95,6 +101,13 @@ public:
virtual void disable() override
{
Logger::info(L"File Locksmith disabled");
+ if (!package::IsWin11OrGreater())
+ {
+#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
+ FileLocksmithRuntimeRegistration::Unregister();
+ Logger::info(L"File Locksmith context menu unregistered (Win10)");
+#endif
+ }
m_enabled = false;
}
diff --git a/src/modules/FileLocksmith/FileLocksmithExt/RuntimeRegistration.h b/src/modules/FileLocksmith/FileLocksmithExt/RuntimeRegistration.h
new file mode 100644
index 0000000000..4dd0d34bea
--- /dev/null
+++ b/src/modules/FileLocksmith/FileLocksmithExt/RuntimeRegistration.h
@@ -0,0 +1,36 @@
+// Header-only runtime registration for FileLocksmith context menu extension.
+#pragma once
+
+#include
+
+namespace globals { extern HMODULE instance; }
+
+namespace FileLocksmithRuntimeRegistration
+{
+ namespace
+ {
+ inline runtime_shell_ext::Spec BuildSpec()
+ {
+ runtime_shell_ext::Spec spec;
+ spec.clsid = L"{84D68575-E186-46AD-B0CB-BAEB45EE29C0}";
+ spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\FileLocksmith";
+ spec.sentinelValue = L"ContextMenuRegistered";
+ spec.dllFileCandidates = { L"PowerToys.FileLocksmithExt.dll" };
+ spec.contextMenuHandlerKeyPaths = {
+ L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt",
+ L"Software\\Classes\\Drive\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt" };
+ spec.friendlyName = L"File Locksmith Shell Extension";
+ return spec;
+ }
+ }
+
+ inline bool EnsureRegistered()
+ {
+ return runtime_shell_ext::EnsureRegistered(BuildSpec(), globals::instance);
+ }
+
+ inline void Unregister()
+ {
+ runtime_shell_ext::Unregister(BuildSpec());
+ }
+}
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp
index 937e9bfca3..61e292d7ee 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp
@@ -27,6 +27,73 @@ 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
{
@@ -69,6 +136,7 @@ private:
bool m_drawing = false;
bool m_destroyed = false;
bool m_hiddenCursor = false;
+ bool m_externalControl = false;
void SetAutoHideTimer() noexcept;
// Configurable Settings
@@ -264,9 +332,12 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP
if (nCode >= 0)
{
MSLLHOOKSTRUCT* hookData = reinterpret_cast(lParam);
- if (wParam == WM_MOUSEMOVE)
+ if (instance && !instance->m_externalControl)
{
- instance->UpdateCrosshairsPosition();
+ if (wParam == WM_MOUSEMOVE)
+ {
+ instance->UpdateCrosshairsPosition();
+ }
}
}
return CallNextHookEx(0, nCode, wParam, lParam);
@@ -527,6 +598,26 @@ 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.");
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h
index 43456a4326..a6618d85bf 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h
@@ -31,3 +31,7 @@ void InclusiveCrosshairsDisable();
bool InclusiveCrosshairsIsEnabled();
void InclusiveCrosshairsSwitch();
void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings);
+void InclusiveCrosshairsRequestUpdatePosition();
+void InclusiveCrosshairsEnsureOn();
+void InclusiveCrosshairsEnsureOff();
+void InclusiveCrosshairsSetExternalControl(bool enabled);
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
index d2273c7efd..3dcee0d6a4 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
@@ -4,6 +4,15 @@
#include "trace.h"
#include "InclusiveCrosshairs.h"
#include "common/utils/color.h"
+#include
+#include
+#include
+#include
+
+extern void InclusiveCrosshairsRequestUpdatePosition();
+extern void InclusiveCrosshairsEnsureOn();
+extern void InclusiveCrosshairsEnsureOff();
+extern void InclusiveCrosshairsSetExternalControl(bool enabled);
// Non-Localizable strings
namespace
@@ -11,6 +20,7 @@ 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";
@@ -21,13 +31,15 @@ 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)
@@ -57,8 +69,46 @@ private:
// The PowerToy state.
bool m_enabled = false;
- // Hotkey to invoke the module
- HotkeyEx m_hotkey;
+ // 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 stopX{ false };
+ std::atomic 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 m_state;
+
+ // Worker threads
+ std::thread m_xThread;
+ std::thread m_yThread;
+
+ // Gliding cursor state machine
+ std::atomic 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
// Mouse Pointer Crosshairs specific settings
InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings;
@@ -68,12 +118,17 @@ public:
MousePointerCrosshairs()
{
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName);
+ m_state = std::make_shared();
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;
}
@@ -107,9 +162,7 @@ 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
@@ -143,6 +196,9 @@ public:
{
m_enabled = false;
Trace::EnableMousePointerCrosshairs(false);
+ StopXTimer();
+ StopYTimer();
+ m_glideState = 0;
InclusiveCrosshairsDisable();
}
@@ -158,15 +214,249 @@ public:
return false;
}
- virtual std::optional GetHotkeyEx() override
+ // Legacy multi-hotkey support (like CropAndLock)
+ virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override
{
- return m_hotkey;
+ if (buffer && buffer_size >= 2)
+ {
+ buffer[0] = m_activationHotkey; // Crosshairs toggle
+ buffer[1] = m_glidingHotkey; // Gliding cursor toggle
+ }
+ return 2;
}
- virtual void OnHotkeyEx() override
+ virtual bool on_hotkey(size_t hotkeyId) override
{
- InclusiveCrosshairsSwitch();
+ if (!m_enabled)
+ {
+ return false;
+ }
+
+ if (hotkeyId == 0)
+ {
+ InclusiveCrosshairsSwitch();
+ return true;
+ }
+ if (hotkeyId == 1)
+ {
+ HandleGlidingHotkey();
+ return true;
+ }
+ return false;
}
+
+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& 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(s->currentXSpeed) * kTimerTickMs) / static_cast(kBaseSpeedTickMs);
+ s->xFraction += perTick;
+ int step = static_cast(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& 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(s->currentYSpeed) * kTimerTickMs) / static_cast(kBaseSpeedTickMs);
+ s->yFraction += perTick;
+ int step = static_cast(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 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 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()
{
@@ -192,37 +482,44 @@ public:
{
try
{
- // Parse HotKey
+ // Parse primary activation HotKey (for centralized hook)
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;
- }
- 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();
+ // 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(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(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);
@@ -272,7 +569,6 @@ public:
{
throw std::runtime_error("Invalid Radius value");
}
-
}
catch (...)
{
@@ -291,7 +587,6 @@ public:
{
throw std::runtime_error("Invalid Thickness value");
}
-
}
catch (...)
{
@@ -320,7 +615,7 @@ public:
{
// Parse border size
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE);
- int value = static_cast (jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
+ int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0)
{
inclusiveCrosshairsSettings.crosshairsBorderSize = value;
@@ -383,20 +678,86 @@ public:
{
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(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(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_hotkey.modifiersMask)
+
+ if (m_activationHotkey.key == 0)
{
- 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_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;
}
m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings;
}
-
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
diff --git a/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp b/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp
index 33030fbdfb..29d7a781ae 100644
--- a/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp
+++ b/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp
@@ -556,6 +556,61 @@ public:
return m_enabled;
}
+ virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
+ {
+ constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
+
+ if (hotkeys && buffer_size >= num_hotkeys)
+ {
+ PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
+
+ // Cache the raw JSON object to avoid multiple parsing
+ json::JsonObject root_json = values.get_raw_json();
+ json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
+
+ size_t hotkey_index = 0;
+
+ // Helper lambda to extract hotkey from JSON properties
+ auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey {
+ if (properties_json.HasKey(property_name))
+ {
+ try
+ {
+ json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
+
+ // Extract hotkey properties directly from JSON
+ bool win = hotkey_json.GetNamedBoolean(L"win", false);
+ bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false);
+ bool alt = hotkey_json.GetNamedBoolean(L"alt", false);
+ bool shift = hotkey_json.GetNamedBoolean(L"shift", false);
+ unsigned char key = static_cast(
+ hotkey_json.GetNamedNumber(L"code", 0));
+
+ return { win, ctrl, shift, alt, key };
+ }
+ catch (...)
+ {
+ // If parsing individual hotkey fails, use defaults
+ return { false, false, false, false, 0 };
+ }
+ }
+ else
+ {
+ // Property doesn't exist, use defaults
+ return { false, false, false, false, 0 };
+ }
+ };
+
+ // Extract all hotkeys using the optimized helper
+ hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse
+ hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine
+ hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs
+ hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect
+ }
+
+ return num_hotkeys;
+ }
+
void launch_add_firewall_process()
{
Logger::trace(L"Starting Process to add firewall rule");
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
index 9ac251c8ec..90058a503e 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
@@ -123,6 +123,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv
+
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters
index 7d014eb00f..d8b2eaf9ea 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters
@@ -84,6 +84,9 @@
Header Files
+
+ Header Files
+
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/RuntimeRegistration.h b/src/modules/NewPlus/NewShellExtensionContextMenu/RuntimeRegistration.h
new file mode 100644
index 0000000000..91596d9e3d
--- /dev/null
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/RuntimeRegistration.h
@@ -0,0 +1,36 @@
+// Header-only runtime registration for New+ Win10 context menu.
+#pragma once
+
+#include
+#include
+#include
+
+// Provided by dll_main.cpp
+extern HMODULE module_instance_handle;
+
+namespace NewPlusRuntimeRegistration
+{
+ namespace {
+ inline runtime_shell_ext::Spec BuildSpec()
+ {
+ runtime_shell_ext::Spec spec;
+ spec.clsid = L"{FF90D477-E32A-4BE8-8CC5-A502A97F5401}";
+ spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\NewPlus";
+ spec.sentinelValue = L"ContextMenuRegisteredWin10";
+ spec.dllFileCandidates = { L"PowerToys.NewPlus.ShellExtension.win10.dll" };
+ spec.contextMenuHandlerKeyPaths = { L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\NewPlusShellExtensionWin10" };
+ spec.friendlyName = L"NewPlus Shell Extension Win10";
+ return spec;
+ }
+ }
+
+ inline bool EnsureRegisteredWin10()
+ {
+ return runtime_shell_ext::EnsureRegistered(BuildSpec(), module_instance_handle);
+ }
+
+ inline void Unregister()
+ {
+ runtime_shell_ext::Unregister(BuildSpec());
+ }
+}
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp
index 303f072e3b..ad94431953 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp
@@ -16,6 +16,7 @@
#include "trace.h"
#include "new_utilities.h"
#include "Generated Files/resource.h"
+#include "RuntimeRegistration.h"
// Note: Settings are managed via Settings and UI Settings
class NewModule : public PowertoyModuleIface
@@ -93,8 +94,16 @@ public:
// Log telemetry
Trace::EventToggleOnOff(true);
-
- newplus::utilities::register_msix_package();
+ if (package::IsWin11OrGreater())
+ {
+ newplus::utilities::register_msix_package();
+ }
+ else
+ {
+#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
+ NewPlusRuntimeRegistration::EnsureRegisteredWin10();
+#endif
+ }
powertoy_new_enabled = true;
}
@@ -141,6 +150,13 @@ private:
{
Trace::EventToggleOnOff(false);
}
+ if (!package::IsWin11OrGreater())
+ {
+#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
+ NewPlusRuntimeRegistration::Unregister();
+ Logger::info(L"New+ context menu unregistered (Win10)");
+#endif
+ }
powertoy_new_enabled = false;
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs
index 4f589a4e2f..22dad16504 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs
@@ -189,7 +189,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
else
{
- return new SeparatorContextItemViewModel() as IContextItemViewModel;
+ return new SeparatorViewModel() as IContextItemViewModel;
}
})
.ToList();
@@ -350,7 +350,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
else
{
- return new SeparatorContextItemViewModel() as IContextItemViewModel;
+ return new SeparatorViewModel() as IContextItemViewModel;
}
})
.ToList();
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs
index 0c0f7c7c12..9665908474 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs
@@ -119,7 +119,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
}
else
{
- return new SeparatorContextItemViewModel();
+ return new SeparatorViewModel();
}
})
.ToList();
@@ -178,7 +178,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
}
else
{
- return new SeparatorContextItemViewModel();
+ return new SeparatorViewModel();
}
})
.ToList();
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs
new file mode 100644
index 0000000000..78fdb26286
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs
@@ -0,0 +1,57 @@
+// 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.Core.ViewModels.Models;
+using Microsoft.CommandPalette.Extensions;
+
+namespace Microsoft.CmdPal.Core.ViewModels;
+
+public partial class FilterItemViewModel : ExtensionObjectViewModel, IFilterItemViewModel
+{
+ private ExtensionObject _model;
+
+ public string Id { get; set; } = string.Empty;
+
+ public string Name { get; set; } = string.Empty;
+
+ public IconInfoViewModel Icon { get; set; } = new(null);
+
+ internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
+
+ protected bool IsInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.Initialized);
+
+ public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error);
+
+ public FilterItemViewModel(IFilter filter, WeakReference context)
+ : base(context)
+ {
+ _model = new(filter);
+ }
+
+ public override void InitializeProperties()
+ {
+ if (IsInitialized)
+ {
+ return;
+ }
+
+ var filter = _model.Unsafe;
+ if (filter == null)
+ {
+ return; // throw?
+ }
+
+ Id = filter.Id;
+ Name = filter.Name;
+ Icon = new(filter.Icon);
+ if (Icon is not null)
+ {
+ Icon.InitializeProperties();
+ }
+
+ UpdateProperty(nameof(Id));
+ UpdateProperty(nameof(Name));
+ UpdateProperty(nameof(Icon));
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs
new file mode 100644
index 0000000000..511581558c
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs
@@ -0,0 +1,81 @@
+// 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 CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.CmdPal.Core.ViewModels.Models;
+using Microsoft.CommandPalette.Extensions;
+
+namespace Microsoft.CmdPal.Core.ViewModels;
+
+public partial class FiltersViewModel : ExtensionObjectViewModel
+{
+ private readonly ExtensionObject _filtersModel = new(null);
+
+ [ObservableProperty]
+ public partial string CurrentFilterId { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(ShouldShowFilters))]
+ public partial IFilterItemViewModel[] Filters { get; set; } = [];
+
+ public bool ShouldShowFilters => Filters.Length > 0;
+
+ public FiltersViewModel(ExtensionObject filters, WeakReference context)
+ : base(context)
+ {
+ _filtersModel = filters;
+ }
+
+ public override void InitializeProperties()
+ {
+ try
+ {
+ if (_filtersModel.Unsafe is not null)
+ {
+ var filters = _filtersModel.Unsafe.GetFilters();
+ Filters = filters.Select(filter =>
+ {
+ var filterItem = filter as IFilter;
+ if (filterItem != null)
+ {
+ var filterVM = new FilterItemViewModel(filterItem!, PageContext);
+ filterVM.InitializeProperties();
+
+ return filterVM;
+ }
+ else
+ {
+ return new SeparatorViewModel();
+ }
+ }).ToArray();
+
+ CurrentFilterId = _filtersModel.Unsafe.CurrentFilterId;
+
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ ShowException(ex, _filtersModel.Unsafe?.GetType().Name);
+ }
+
+ Filters = [];
+ CurrentFilterId = string.Empty;
+ }
+
+ public override void SafeCleanup()
+ {
+ base.SafeCleanup();
+
+ foreach (var filter in Filters)
+ {
+ if (filter is FilterItemViewModel filterVM)
+ {
+ filterVM.SafeCleanup();
+ }
+ }
+
+ Filters = [];
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs
index cad1af9d4d..a8f65b2634 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs
@@ -2,12 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace Microsoft.CmdPal.Core.ViewModels;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs
new file mode 100644
index 0000000000..fb324bb42f
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs
@@ -0,0 +1,9 @@
+// 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.Core.ViewModels;
+
+public interface IFilterItemViewModel
+{
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs
index 10fc9445ad..f89b2a5906 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs
@@ -26,6 +26,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
[ObservableProperty]
public partial ObservableCollection FilteredItems { get; set; } = [];
+ public FiltersViewModel? Filters { get; set; }
+
private ObservableCollection Items { get; set; } = [];
private readonly ExtensionObject _model;
@@ -86,7 +88,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems();
- protected override void OnFilterUpdated(string filter)
+ protected override void OnSearchTextBoxUpdated(string searchTextBox)
{
//// TODO: Just temp testing, need to think about where we want to filter, as AdvancedCollectionView in View could be done, but then grouping need CollectionViewSource, maybe we do grouping in view
//// and manage filtering below, but we should be smarter about this and understand caching and other requirements...
@@ -104,7 +106,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
if (_model.Unsafe is IDynamicListPage dynamic)
{
- dynamic.SearchText = filter;
+ dynamic.SearchText = searchTextBox;
}
}
catch (Exception ex)
@@ -127,6 +129,26 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
}
+ public void UpdateCurrentFilter(string currentFilterId)
+ {
+ // We're getting called on the UI thread.
+ // Hop off to a BG thread to update the extension.
+ _ = Task.Run(() =>
+ {
+ try
+ {
+ if (_model.Unsafe is IListPage listPage)
+ {
+ listPage.Filters?.CurrentFilterId = currentFilterId;
+ }
+ }
+ catch (Exception ex)
+ {
+ ShowException(ex, _model?.Unsafe?.Name);
+ }
+ });
+ }
+
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
private void FetchItems()
{
@@ -305,7 +327,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
/// Apply our current filter text to the list of items, and update
/// FilteredItems to match the results.
///
- private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, Filter));
+ private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, SearchTextBox));
///
/// Helper to generate a weighting for a given list item, based on title,
@@ -507,6 +529,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.SlowInitializeProperties();
+ Filters = new(new(model.Filters), PageContext);
+ Filters.InitializeProperties();
+ UpdateProperty(nameof(Filters));
+
FetchItems();
model.ItemsChanged += Model_ItemsChanged;
}
@@ -578,6 +604,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.SlowInitializeProperties();
break;
+ case nameof(Filters):
+ Filters = new(new(model.Filters), PageContext);
+ Filters.InitializeProperties();
+ break;
case nameof(IsLoading):
UpdateEmptyContent();
break;
@@ -641,6 +671,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
FilteredItems.Clear();
}
+ Filters?.SafeCleanup();
+
var model = _model.Unsafe;
if (model is not null)
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
index 046c9fae93..5c445615be 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
@@ -32,7 +32,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
// This is set from the SearchBar
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowSuggestion))]
- public partial string Filter { get; set; } = string.Empty;
+ public partial string SearchTextBox { get; set; } = string.Empty;
[ObservableProperty]
public virtual partial string PlaceholderText { get; private set; } = "Type here to search...";
@@ -41,7 +41,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
[NotifyPropertyChangedFor(nameof(ShowSuggestion))]
public virtual partial string TextToSuggest { get; protected set; } = string.Empty;
- public bool ShowSuggestion => !string.IsNullOrEmpty(TextToSuggest) && TextToSuggest != Filter;
+ public bool ShowSuggestion => !string.IsNullOrEmpty(TextToSuggest) && TextToSuggest != SearchTextBox;
[ObservableProperty]
public partial AppExtensionHost ExtensionHost { get; private set; }
@@ -167,9 +167,9 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
}
}
- partial void OnFilterChanged(string oldValue, string newValue) => OnFilterUpdated(newValue);
+ partial void OnSearchTextBoxChanged(string oldValue, string newValue) => OnSearchTextBoxUpdated(newValue);
- protected virtual void OnFilterUpdated(string filter)
+ protected virtual void OnSearchTextBoxUpdated(string searchTextBox)
{
// The base page has no notion of data, so we do nothing here...
// subclasses should override.
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs
similarity index 73%
rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs
rename to src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs
index 8d896bd341..a1c4696b35 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs
@@ -9,6 +9,10 @@ using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
-public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem
+public partial class SeparatorViewModel() :
+ IContextItemViewModel,
+ IFilterItemViewModel,
+ ISeparatorContextItem,
+ ISeparatorFilterItem
{
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs
index 642e5ad4a9..72a295c83f 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs
@@ -97,14 +97,27 @@ public partial class AliasManager : ObservableObject
}
}
- // Look for the old alias, and remove it
List toRemove = [];
foreach (var kv in _aliases)
{
+ // Look for the old aliases for the command, and remove it
if (kv.Value.CommandId == commandId)
{
toRemove.Add(kv.Value);
}
+
+ // Look for the alias belonging to another command, and remove it
+ if (newAlias is not null && kv.Value.Alias == newAlias.Alias)
+ {
+ toRemove.Add(kv.Value);
+
+ // Remove alias from other TopLevelViewModels it may be assigned to
+ var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
+ if (topLevelCommand is not null)
+ {
+ topLevelCommand.AliasText = string.Empty;
+ }
+ }
}
foreach (var alias in toRemove)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
index 9728e8339e..9b2234fb16 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Templating;
@@ -96,109 +97,40 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference()`
- // or similar will throw a System.InvalidCastException.
- //
- // Instead we have this horror show.
- //
- // The `action.ToJson()` blob ACTUALLY CONTAINS THE `type` field, which
- // we can use to determine what kind of action it is. Then we can parse
- // the JSON manually based on the type.
- var actionJson = action.ToJson();
-
- if (actionJson.TryGetValue("type", out var actionTypeValue))
+ if (action is AdaptiveOpenUrlAction openUrlAction)
{
- var actionTypeString = actionTypeValue.GetString();
- Logger.LogTrace($"atString={actionTypeString}");
-
- var actionType = actionTypeString switch
- {
- "Action.Submit" => ActionType.Submit,
- "Action.Execute" => ActionType.Execute,
- "Action.OpenUrl" => ActionType.OpenUrl,
- _ => ActionType.Unsupported,
- };
-
- Logger.LogDebug($"{actionTypeString}->{actionType}");
-
- switch (actionType)
- {
- case ActionType.OpenUrl:
- {
- HandleOpenUrlAction(action, actionJson);
- }
-
- break;
- case ActionType.Submit:
- case ActionType.Execute:
- {
- HandleSubmitAction(action, actionJson, inputs);
- }
-
- break;
- default:
- Logger.LogError($"{actionType} was an unexpected action `type`");
- break;
- }
- }
- else
- {
- Logger.LogError($"actionJson.TryGetValue(type) failed");
- }
- }
-
- private void HandleOpenUrlAction(IAdaptiveActionElement action, JsonObject actionJson)
- {
- if (actionJson.TryGetValue("url", out var actionUrlValue))
- {
- var actionUrl = actionUrlValue.GetString() ?? string.Empty;
- if (Uri.TryCreate(actionUrl, default(UriCreationOptions), out var uri))
- {
- WeakReferenceMessenger.Default.Send(new(uri));
- }
- else
- {
- Logger.LogError($"Failed to produce URI for {actionUrlValue}");
- }
- }
- }
-
- private void HandleSubmitAction(
- IAdaptiveActionElement action,
- JsonObject actionJson,
- JsonObject inputs)
- {
- var dataString = string.Empty;
- if (actionJson.TryGetValue("data", out var actionDataValue))
- {
- dataString = actionDataValue.Stringify() ?? string.Empty;
+ WeakReferenceMessenger.Default.Send(new(openUrlAction.Url));
+ return;
}
- var inputString = inputs.Stringify();
- _ = Task.Run(() =>
+ if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
{
- try
+ // Get the data and inputs
+ var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
+ var inputString = inputs.Stringify();
+
+ _ = Task.Run(() =>
{
- var model = _formModel.Unsafe!;
- if (model != null)
+ try
{
- var result = model.SubmitForm(inputString, dataString);
- Logger.LogDebug($"SubmitForm() returned {result}");
- WeakReferenceMessenger.Default.Send(new(new(result)));
+ var model = _formModel.Unsafe!;
+ if (model != null)
+ {
+ var result = model.SubmitForm(inputString, dataString);
+ WeakReferenceMessenger.Default.Send(new(new(result)));
+ }
}
- }
- catch (Exception ex)
- {
- ShowException(ex);
- }
- });
+ catch (Exception ex)
+ {
+ ShowException(ex);
+ }
+ });
+ }
}
private static readonly string ErrorCardJson = """
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
index e73f5b09ba..e9b3f1bcca 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
@@ -110,6 +110,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
get => Alias?.Alias ?? string.Empty;
set
{
+ var previousAlias = Alias?.Alias ?? string.Empty;
+
if (string.IsNullOrEmpty(value))
{
Alias = null;
@@ -126,9 +128,13 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
}
}
- HandleChangeAlias();
- OnPropertyChanged(nameof(AliasText));
- OnPropertyChanged(nameof(IsDirectAlias));
+ // Only call HandleChangeAlias if there was an actual change.
+ if (previousAlias != Alias?.Alias)
+ {
+ HandleChangeAlias();
+ OnPropertyChanged(nameof(AliasText));
+ OnPropertyChanged(nameof(IsDirectAlias));
+ }
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
index 49fef61ecb..aba9ed477d 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
@@ -41,14 +41,18 @@
+
+
+
+ ShouldConstrainToRootBounds="False"
+ SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
@@ -161,6 +165,7 @@
x:Name="PrimaryButton"
Padding="6,4,4,4"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
+ AutomationProperties.AutomationId="PrimaryCommandButton"
AutomationProperties.Name="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}"
Background="Transparent"
Click="PrimaryButton_Clicked"
@@ -180,6 +185,7 @@
x:Name="SecondaryButton"
Padding="6,4,4,4"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
+ AutomationProperties.AutomationId="SecondaryCommandButton"
AutomationProperties.Name="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}"
Click="SecondaryButton_Clicked"
Style="{StaticResource SubtleButtonStyle}"
@@ -203,6 +209,7 @@
x:Name="MoreCommandsButton"
x:Uid="MoreCommandsButton"
Padding="6,4,4,4"
+ AutomationProperties.AutomationId="MoreContextMenuButton"
Click="MoreCommandsButton_Clicked"
Style="{StaticResource SubtleButtonStyle}"
ToolTipService.ToolTip="Ctrl+K"
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml
index f3c4e5413e..e23fb815b0 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml
@@ -108,7 +108,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs
new file mode 100644
index 0000000000..b51376dfa3
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs
@@ -0,0 +1,189 @@
+// 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.Core.ViewModels;
+using Microsoft.CmdPal.UI.Views;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
+using Windows.System;
+
+namespace Microsoft.CmdPal.UI.Controls;
+
+public sealed partial class FiltersDropDown : UserControl,
+ ICurrentPageAware
+{
+ public PageViewModel? CurrentPageViewModel
+ {
+ get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
+ set => SetValue(CurrentPageViewModelProperty, value);
+ }
+
+ public static readonly DependencyProperty CurrentPageViewModelProperty =
+ DependencyProperty.Register(nameof(CurrentPageViewModel), typeof(PageViewModel), typeof(FiltersDropDown), new PropertyMetadata(null, OnCurrentPageViewModelChanged));
+
+ private static void OnCurrentPageViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var @this = (FiltersDropDown)d;
+
+ if (@this != null
+ && e.OldValue is PageViewModel old)
+ {
+ old.PropertyChanged -= @this.Page_PropertyChanged;
+ }
+
+ // If this new page does not implement ListViewModel or if
+ // it doesn't contain Filters, we need to clear any filters
+ // that may have been set.
+ if (@this != null)
+ {
+ if (e.NewValue is ListViewModel listViewModel)
+ {
+ @this.ViewModel = listViewModel.Filters;
+ }
+ else
+ {
+ @this.ViewModel = null;
+ }
+ }
+
+ if (@this != null
+ && e.NewValue is PageViewModel page)
+ {
+ page.PropertyChanged += @this.Page_PropertyChanged;
+ }
+ }
+
+ public FiltersViewModel? ViewModel
+ {
+ get => (FiltersViewModel?)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ public static readonly DependencyProperty ViewModelProperty =
+ DependencyProperty.Register(nameof(ViewModel), typeof(FiltersViewModel), typeof(FiltersDropDown), new PropertyMetadata(null));
+
+ public FiltersDropDown()
+ {
+ this.InitializeComponent();
+ }
+
+ // Used to handle the case when a ListPage's `Filters` may have changed
+ private void Page_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ var property = e.PropertyName;
+
+ if (CurrentPageViewModel is ListViewModel list)
+ {
+ if (property == nameof(ListViewModel.Filters))
+ {
+ ViewModel = list.Filters;
+ }
+ }
+ }
+
+ private void FiltersComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (CurrentPageViewModel is ListViewModel listViewModel &&
+ FiltersComboBox.SelectedItem is FilterItemViewModel filterItem)
+ {
+ listViewModel.UpdateCurrentFilter(filterItem.Id);
+ }
+ }
+
+ private void FiltersComboBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (e.Key == VirtualKey.Up)
+ {
+ NavigateUp();
+
+ e.Handled = true;
+ }
+ else if (e.Key == VirtualKey.Down)
+ {
+ NavigateDown();
+
+ e.Handled = true;
+ }
+ }
+
+ private void NavigateUp()
+ {
+ var newIndex = FiltersComboBox.SelectedIndex;
+
+ if (FiltersComboBox.SelectedIndex > 0)
+ {
+ newIndex--;
+
+ while (
+ newIndex >= 0 &&
+ IsSeparator(FiltersComboBox.Items[newIndex]) &&
+ newIndex != FiltersComboBox.SelectedIndex)
+ {
+ newIndex--;
+ }
+
+ if (newIndex < 0)
+ {
+ newIndex = FiltersComboBox.Items.Count - 1;
+
+ while (
+ newIndex >= 0 &&
+ IsSeparator(FiltersComboBox.Items[newIndex]) &&
+ newIndex != FiltersComboBox.SelectedIndex)
+ {
+ newIndex--;
+ }
+ }
+ }
+ else
+ {
+ newIndex = FiltersComboBox.Items.Count - 1;
+ }
+
+ FiltersComboBox.SelectedIndex = newIndex;
+ }
+
+ private void NavigateDown()
+ {
+ var newIndex = FiltersComboBox.SelectedIndex;
+
+ if (FiltersComboBox.SelectedIndex == FiltersComboBox.Items.Count - 1)
+ {
+ newIndex = 0;
+ }
+ else
+ {
+ newIndex++;
+
+ while (
+ newIndex < FiltersComboBox.Items.Count &&
+ IsSeparator(FiltersComboBox.Items[newIndex]) &&
+ newIndex != FiltersComboBox.SelectedIndex)
+ {
+ newIndex++;
+ }
+
+ if (newIndex >= FiltersComboBox.Items.Count)
+ {
+ newIndex = 0;
+
+ while (
+ newIndex < FiltersComboBox.Items.Count &&
+ IsSeparator(FiltersComboBox.Items[newIndex]) &&
+ newIndex != FiltersComboBox.SelectedIndex)
+ {
+ newIndex++;
+ }
+ }
+ }
+
+ FiltersComboBox.SelectedIndex = newIndex;
+ }
+
+ private bool IsSeparator(object item)
+ {
+ return item is SeparatorViewModel;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
index 379ea6b03d..80eb1a3ad6 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
@@ -22,6 +22,7 @@
MinHeight="32"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"
+ AutomationProperties.AutomationId="MainSearchBox"
KeyDown="FilterBox_KeyDown"
PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}"
PreviewKeyDown="FilterBox_PreviewKeyDown"
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
index 7b34594b46..a5f02d76cb 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
@@ -62,7 +62,7 @@ public sealed partial class SearchBar : UserControl,
{
// TODO: In some cases we probably want commands to clear a filter
// somewhere in the process, so we need to figure out when that is.
- @this.FilterBox.Text = page.Filter;
+ @this.FilterBox.Text = page.SearchTextBox;
@this.FilterBox.Select(@this.FilterBox.Text.Length, 0);
page.PropertyChanged += @this.Page_PropertyChanged;
@@ -87,7 +87,7 @@ public sealed partial class SearchBar : UserControl,
if (CurrentPageViewModel is not null)
{
- CurrentPageViewModel.Filter = string.Empty;
+ CurrentPageViewModel.SearchTextBox = string.Empty;
}
}));
}
@@ -145,7 +145,7 @@ public sealed partial class SearchBar : UserControl,
// hack TODO GH #245
if (CurrentPageViewModel is not null)
{
- CurrentPageViewModel.Filter = FilterBox.Text;
+ CurrentPageViewModel.SearchTextBox = FilterBox.Text;
}
}
@@ -156,7 +156,7 @@ public sealed partial class SearchBar : UserControl,
// hack TODO GH #245
if (CurrentPageViewModel is not null)
{
- CurrentPageViewModel.Filter = FilterBox.Text;
+ CurrentPageViewModel.SearchTextBox = FilterBox.Text;
}
}
}
@@ -320,7 +320,7 @@ public sealed partial class SearchBar : UserControl,
// Actually plumb Filtering to the view model
if (CurrentPageViewModel is not null)
{
- CurrentPageViewModel.Filter = FilterBox.Text;
+ CurrentPageViewModel.SearchTextBox = FilterBox.Text;
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs
index 3ab4dd1c0b..09fa902a4b 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs
@@ -28,7 +28,7 @@ internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
{
li.IsEnabled = true;
- if (item is SeparatorContextItemViewModel)
+ if (item is SeparatorViewModel)
{
li.IsEnabled = false;
li.AllowFocusWhenDisabled = false;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs
new file mode 100644
index 0000000000..2d12c82b28
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs
@@ -0,0 +1,36 @@
+// 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.Core.ViewModels;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Microsoft.CmdPal.UI;
+
+internal sealed partial class FilterTemplateSelector : DataTemplateSelector
+{
+ public DataTemplate? Default { get; set; }
+
+ public DataTemplate? Separator { get; set; }
+
+ protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
+ {
+ DataTemplate? dataTemplate = Default;
+
+ if (dependencyObject is ComboBoxItem comboBoxItem)
+ {
+ comboBoxItem.IsEnabled = true;
+
+ if (item is SeparatorViewModel)
+ {
+ comboBoxItem.IsEnabled = false;
+ comboBoxItem.AllowFocusWhenDisabled = false;
+ comboBoxItem.AllowFocusOnInteraction = false;
+ dataTemplate = Separator;
+ }
+ }
+
+ return dataTemplate;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TrayIconService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TrayIconService.cs
index 224c851ff1..442341cc5e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TrayIconService.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TrayIconService.cs
@@ -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_Exit"));
+ PInvoke.InsertMenu(_popupMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 2, RS_.GetString("TrayMenu_Close"));
}
}
else
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
index 8f7c6ac7bd..dbb3818518 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
@@ -176,6 +176,7 @@
+
@@ -320,6 +321,18 @@
+
+
+
+
+
+
+
+
+
Settings
-
- Exit
+
+ Close
+ Close as a verb, as in Close the application
Direct
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs
new file mode 100644
index 0000000000..e7fbc6859d
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
+
+[TestClass]
+public class AllAppsCommandProviderTests : AppsTestBase
+{
+ [TestMethod]
+ public void ProviderHasDisplayName()
+ {
+ // Setup
+ var provider = new AllAppsCommandProvider();
+
+ // Assert
+ Assert.IsNotNull(provider.DisplayName);
+ Assert.IsTrue(provider.DisplayName.Length > 0);
+ }
+
+ [TestMethod]
+ public void ProviderHasIcon()
+ {
+ // Setup
+ var provider = new AllAppsCommandProvider();
+
+ // Assert
+ Assert.IsNotNull(provider.Icon);
+ }
+
+ [TestMethod]
+ public void TopLevelCommandsNotEmpty()
+ {
+ // Setup
+ var provider = new AllAppsCommandProvider();
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+ Assert.IsTrue(commands.Length > 0);
+ }
+
+ [TestMethod]
+ public void LookupAppWithEmptyNameReturnsNotNull()
+ {
+ // Setup
+ var mockApp = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
+ MockCache.AddWin32Program(mockApp);
+ var page = new AllAppsPage(MockCache);
+
+ var provider = new AllAppsCommandProvider(page);
+
+ // Act
+ var result = provider.LookupApp(string.Empty);
+
+ // Assert
+ Assert.IsNotNull(result);
+ }
+
+ [TestMethod]
+ public async Task ProviderWithMockData_LookupApp_ReturnsCorrectApp()
+ {
+ // Arrange
+ var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
+ MockCache.AddWin32Program(testApp);
+
+ var provider = new AllAppsCommandProvider(Page);
+
+ // Wait for initialization to complete
+ await WaitForPageInitializationAsync();
+
+ // Act
+ var result = provider.LookupApp("TestApp");
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual("TestApp", result.Title);
+ }
+
+ [TestMethod]
+ public async Task ProviderWithMockData_LookupApp_ReturnsNullForNonExistentApp()
+ {
+ // Arrange
+ var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
+ MockCache.AddWin32Program(testApp);
+
+ var provider = new AllAppsCommandProvider(Page);
+
+ // Wait for initialization to complete
+ await WaitForPageInitializationAsync();
+
+ // Act
+ var result = provider.LookupApp("NonExistentApp");
+
+ // Assert
+ Assert.IsNull(result);
+ }
+
+ [TestMethod]
+ public void ProviderWithMockData_TopLevelCommands_IncludesListItem()
+ {
+ // Arrange
+ var provider = new AllAppsCommandProvider(Page);
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+ Assert.IsTrue(commands.Length >= 1); // At least the list item should be present
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs
new file mode 100644
index 0000000000..3ac1eaff68
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
+
+[TestClass]
+public class AllAppsPageTests : AppsTestBase
+{
+ [TestMethod]
+ public void AllAppsPage_Constructor_ThrowsOnNullAppCache()
+ {
+ // Act & Assert
+ Assert.ThrowsException(() => 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);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AppsTestBase.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AppsTestBase.cs
new file mode 100644
index 0000000000..4d1210db7b
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AppsTestBase.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
+
+///
+/// Base class for Apps unit tests that provides common setup and teardown functionality.
+///
+public abstract class AppsTestBase
+{
+ ///
+ /// Gets the mock application cache used in tests.
+ ///
+ protected MockAppCache MockCache { get; private set; } = null!;
+
+ ///
+ /// Gets the AllAppsPage instance used in tests.
+ ///
+ protected AllAppsPage Page { get; private set; } = null!;
+
+ ///
+ /// Sets up the test environment before each test method.
+ ///
+ /// A task representing the asynchronous setup operation.
+ [TestInitialize]
+ public virtual async Task Setup()
+ {
+ MockCache = new MockAppCache();
+ Page = new AllAppsPage(MockCache);
+
+ // Ensure initialization is complete
+ await MockCache.RefreshAsync();
+ }
+
+ ///
+ /// Cleans up the test environment after each test method.
+ ///
+ [TestCleanup]
+ public virtual void Cleanup()
+ {
+ MockCache?.Dispose();
+ }
+
+ ///
+ /// Forces synchronous initialization of the page for testing.
+ ///
+ protected void EnsurePageInitialized()
+ {
+ // Trigger BuildListItems by accessing items
+ _ = Page.GetItems();
+ }
+
+ ///
+ /// Waits for page initialization with timeout.
+ ///
+ /// The timeout in milliseconds.
+ /// A task representing the asynchronous wait operation.
+ protected async Task WaitForPageInitializationAsync(int timeoutMs = 1000)
+ {
+ await MockCache.RefreshAsync();
+ EnsurePageInitialized();
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Microsoft.CmdPal.Ext.Apps.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Microsoft.CmdPal.Ext.Apps.UnitTests.csproj
new file mode 100644
index 0000000000..d6a9638378
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Microsoft.CmdPal.Ext.Apps.UnitTests.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+ false
+ true
+ Microsoft.CmdPal.Ext.Apps.UnitTests
+ $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockAppCache.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockAppCache.cs
new file mode 100644
index 0000000000..03530cb5ce
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockAppCache.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.CmdPal.Ext.Apps.Programs;
+
+namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
+
+///
+/// Mock implementation of IAppCache for unit testing.
+///
+public class MockAppCache : IAppCache
+{
+ private readonly List _win32s = new();
+ private readonly List _uwps = new();
+ private bool _disposed;
+ private bool _shouldReload;
+
+ ///
+ /// Gets the collection of Win32 programs.
+ ///
+ public IList Win32s => _win32s.AsReadOnly();
+
+ ///
+ /// Gets the collection of UWP applications.
+ ///
+ public IList UWPs => _uwps.AsReadOnly();
+
+ ///
+ /// Determines whether the cache should be reloaded.
+ ///
+ /// True if cache should be reloaded, false otherwise.
+ public bool ShouldReload() => _shouldReload;
+
+ ///
+ /// Resets the reload flag.
+ ///
+ public void ResetReloadFlag() => _shouldReload = false;
+
+ ///
+ /// Asynchronously refreshes the cache.
+ ///
+ /// A task representing the asynchronous refresh operation.
+ public async Task RefreshAsync()
+ {
+ // Simulate minimal async operation for testing
+ await Task.Delay(1);
+ }
+
+ ///
+ /// Adds a Win32 program to the cache.
+ ///
+ /// The Win32 program to add.
+ /// Thrown when program is null.
+ public void AddWin32Program(Win32Program program)
+ {
+ ArgumentNullException.ThrowIfNull(program);
+
+ _win32s.Add(program);
+ }
+
+ ///
+ /// Adds a UWP application to the cache.
+ ///
+ /// The UWP application to add.
+ /// Thrown when app is null.
+ public void AddUWPApplication(IUWPApplication app)
+ {
+ ArgumentNullException.ThrowIfNull(app);
+
+ _uwps.Add(app);
+ }
+
+ ///
+ /// Clears all applications from the cache.
+ ///
+ public void ClearAll()
+ {
+ _win32s.Clear();
+ _uwps.Clear();
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ // Clean up managed resources
+ _win32s.Clear();
+ _uwps.Clear();
+ }
+
+ _disposed = true;
+ }
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockUWPApplication.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockUWPApplication.cs
new file mode 100644
index 0000000000..ae39e70fef
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockUWPApplication.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CmdPal.Ext.Apps.Programs;
+using Microsoft.CmdPal.Ext.Apps.Utils;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
+
+///
+/// Mock implementation of IUWPApplication for unit testing.
+///
+public class MockUWPApplication : IUWPApplication
+{
+ ///
+ /// Gets or sets the app list entry.
+ ///
+ public string AppListEntry { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the unique identifier.
+ ///
+ public string UniqueIdentifier { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the display name.
+ ///
+ public string DisplayName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the description.
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the user model ID.
+ ///
+ public string UserModelId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the background color.
+ ///
+ public string BackgroundColor { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the entry point.
+ ///
+ public string EntryPoint { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether the application is enabled.
+ ///
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether the application can run elevated.
+ ///
+ public bool CanRunElevated { get; set; }
+
+ ///
+ /// Gets or sets the logo path.
+ ///
+ public string LogoPath { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the logo type.
+ ///
+ public LogoType LogoType { get; set; } = LogoType.Colored;
+
+ ///
+ /// Gets or sets the UWP package.
+ ///
+ public UWP Package { get; set; } = null!;
+
+ ///
+ /// Gets the name of the application.
+ ///
+ public string Name => DisplayName;
+
+ ///
+ /// Gets the location of the application.
+ ///
+ public string Location => Package?.Location ?? string.Empty;
+
+ ///
+ /// Gets the localized location of the application.
+ ///
+ public string LocationLocalized => Package?.LocationLocalized ?? string.Empty;
+
+ ///
+ /// Gets the application identifier.
+ ///
+ /// The user model ID of the application.
+ public string GetAppIdentifier()
+ {
+ return UserModelId;
+ }
+
+ ///
+ /// Gets the commands available for this application.
+ ///
+ /// A list of context items.
+ public List GetCommands()
+ {
+ return new List();
+ }
+
+ ///
+ /// Updates the logo path based on the specified theme.
+ ///
+ /// The theme to use for the logo.
+ public void UpdateLogoPath(Theme theme)
+ {
+ // Mock implementation - no-op for testing
+ }
+
+ ///
+ /// Converts this UWP application to an AppItem.
+ ///
+ /// An AppItem representation of this UWP application.
+ 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(),
+ };
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/QueryTests.cs
new file mode 100644
index 0000000000..e04c678b58
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/QueryTests.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using Microsoft.CmdPal.Ext.UnitTestBase;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
+
+[TestClass]
+public class QueryTests : CommandPaletteUnitTestBase
+{
+ [TestMethod]
+ public void QueryReturnsExpectedResults()
+ {
+ // Arrange
+ var mockCache = new MockAppCache();
+ var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
+ var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
+ mockCache.AddWin32Program(win32App);
+ mockCache.AddUWPApplication(uwpApp);
+
+ for (var i = 0; i < 10; i++)
+ {
+ mockCache.AddWin32Program(TestDataHelper.CreateTestWin32Program($"App{i}"));
+ mockCache.AddUWPApplication(TestDataHelper.CreateTestUWPApplication($"UWP App {i}"));
+ }
+
+ var page = new AllAppsPage(mockCache);
+ var provider = new AllAppsCommandProvider(page);
+
+ // Act
+ var allItems = page.GetItems();
+
+ // Assert
+ var notepadResult = Query("notepad", allItems).FirstOrDefault();
+ Assert.IsNotNull(notepadResult);
+ Assert.AreEqual("Notepad", notepadResult.Title);
+
+ var calculatorResult = Query("cal", allItems).FirstOrDefault();
+ Assert.IsNotNull(calculatorResult);
+ Assert.AreEqual("Calculator", calculatorResult.Title);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Settings.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Settings.cs
new file mode 100644
index 0000000000..b48abaf32a
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Settings.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CmdPal.Ext.Apps.Helpers;
+
+namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
+
+public class Settings : ISettingsInterface
+{
+ private readonly bool enableStartMenuSource;
+ private readonly bool enableDesktopSource;
+ private readonly bool enableRegistrySource;
+ private readonly bool enablePathEnvironmentVariableSource;
+ private readonly List programSuffixes;
+ private readonly List runCommandSuffixes;
+
+ public Settings(
+ bool enableStartMenuSource = true,
+ bool enableDesktopSource = true,
+ bool enableRegistrySource = true,
+ bool enablePathEnvironmentVariableSource = true,
+ List programSuffixes = null,
+ List runCommandSuffixes = null)
+ {
+ this.enableStartMenuSource = enableStartMenuSource;
+ this.enableDesktopSource = enableDesktopSource;
+ this.enableRegistrySource = enableRegistrySource;
+ this.enablePathEnvironmentVariableSource = enablePathEnvironmentVariableSource;
+ this.programSuffixes = programSuffixes ?? new List { "bat", "appref-ms", "exe", "lnk", "url" };
+ this.runCommandSuffixes = runCommandSuffixes ?? new List { "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 ProgramSuffixes => programSuffixes;
+
+ public List 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 { "exe", "bat" },
+ runCommandSuffixes: new List { "exe", "bat", "cmd" });
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/TestDataHelper.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/TestDataHelper.cs
new file mode 100644
index 0000000000..88936e4285
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/TestDataHelper.cs
@@ -0,0 +1,128 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CmdPal.Ext.Apps.Programs;
+
+namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
+
+///
+/// Helper class to create test data for unit tests.
+///
+public static class TestDataHelper
+{
+ ///
+ /// Creates a test Win32 program with the specified parameters.
+ ///
+ /// The name of the application.
+ /// The full path to the application executable.
+ /// A value indicating whether the application is enabled.
+ /// A value indicating whether the application is valid.
+ /// A new Win32Program instance with the specified parameters.
+ 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,
+ };
+ }
+
+ ///
+ /// Creates a test UWP application with the specified parameters.
+ ///
+ /// The display name of the application.
+ /// The user model ID of the application.
+ /// A value indicating whether the application is enabled.
+ /// A new IUWPApplication instance with the specified parameters.
+ 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),
+ };
+ }
+
+ ///
+ /// Creates a mock UWP package for testing purposes.
+ ///
+ /// The display name of the package.
+ /// The user model ID of the package.
+ /// A new UWP package instance.
+ 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,
+ };
+ }
+
+ ///
+ /// Mock implementation of IPackage for testing purposes.
+ ///
+ private sealed class MockPackage : IPackage
+ {
+ ///
+ /// Gets or sets the name of the package.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the full name of the package.
+ ///
+ public string FullName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the family name of the package.
+ ///
+ public string FamilyName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether the package is a framework package.
+ ///
+ public bool IsFramework { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the package is in development mode.
+ ///
+ public bool IsDevelopmentMode { get; set; }
+
+ ///
+ /// Gets or sets the installed location of the package.
+ ///
+ public string InstalledLocation { get; set; } = string.Empty;
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkDataTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkDataTests.cs
new file mode 100644
index 0000000000..2ee3deeb5d
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkDataTests.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
+
+[TestClass]
+public class BookmarkDataTests
+{
+ [TestMethod]
+ public void BookmarkDataWebUrlDetection()
+ {
+ // Act
+ var webBookmark = new BookmarkData
+ {
+ Name = "Test Site",
+ Bookmark = "https://test.com",
+ };
+
+ var nonWebBookmark = new BookmarkData
+ {
+ Name = "Local File",
+ Bookmark = "C:\\temp\\file.txt",
+ };
+
+ var placeholderBookmark = new BookmarkData
+ {
+ Name = "Placeholder",
+ Bookmark = "{Placeholder}",
+ };
+
+ // Assert
+ Assert.IsTrue(webBookmark.IsWebUrl());
+ Assert.IsFalse(webBookmark.IsPlaceholder);
+ Assert.IsFalse(nonWebBookmark.IsWebUrl());
+ Assert.IsFalse(nonWebBookmark.IsPlaceholder);
+
+ Assert.IsTrue(placeholderBookmark.IsPlaceholder);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkJsonParserTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkJsonParserTests.cs
new file mode 100644
index 0000000000..e442818f8a
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkJsonParserTests.cs
@@ -0,0 +1,535 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
+
+[TestClass]
+public class BookmarkJsonParserTests
+{
+ private BookmarkJsonParser _parser;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _parser = new BookmarkJsonParser();
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_ValidJson_ReturnsBookmarks()
+ {
+ // Arrange
+ var json = """
+ {
+ "Data": [
+ {
+ "Name": "Google",
+ "Bookmark": "https://www.google.com"
+ },
+ {
+ "Name": "Local File",
+ "Bookmark": "C:\\temp\\file.txt"
+ }
+ ]
+ }
+ """;
+
+ // Act
+ var result = _parser.ParseBookmarks(json);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(2, result.Data.Count);
+ Assert.AreEqual("Google", result.Data[0].Name);
+ Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
+ Assert.AreEqual("Local File", result.Data[1].Name);
+ Assert.AreEqual("C:\\temp\\file.txt", result.Data[1].Bookmark);
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_EmptyJson_ReturnsEmptyBookmarks()
+ {
+ // Arrange
+ var json = "{}";
+
+ // Act
+ var result = _parser.ParseBookmarks(json);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(0, result.Data.Count);
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_NullJson_ReturnsEmptyBookmarks()
+ {
+ // Act
+ var result = _parser.ParseBookmarks(null);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(0, result.Data.Count);
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_WhitespaceJson_ReturnsEmptyBookmarks()
+ {
+ // Act
+ var result = _parser.ParseBookmarks(" ");
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(0, result.Data.Count);
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_EmptyString_ReturnsEmptyBookmarks()
+ {
+ // Act
+ var result = _parser.ParseBookmarks(string.Empty);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(0, result.Data.Count);
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_InvalidJson_ReturnsEmptyBookmarks()
+ {
+ // Arrange
+ var invalidJson = "{invalid json}";
+
+ // Act
+ var result = _parser.ParseBookmarks(invalidJson);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(0, result.Data.Count);
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_MalformedJson_ReturnsEmptyBookmarks()
+ {
+ // Arrange
+ var malformedJson = """
+ {
+ "Data": [
+ {
+ "Name": "Google",
+ "Bookmark": "https://www.google.com"
+ },
+ {
+ "Name": "Incomplete entry"
+ """;
+
+ // Act
+ var result = _parser.ParseBookmarks(malformedJson);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(0, result.Data.Count);
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_JsonWithTrailingCommas_ParsesSuccessfully()
+ {
+ // Arrange - JSON with trailing commas (should be handled by AllowTrailingCommas option)
+ var json = """
+ {
+ "Data": [
+ {
+ "Name": "Google",
+ "Bookmark": "https://www.google.com",
+ },
+ {
+ "Name": "Local File",
+ "Bookmark": "C:\\temp\\file.txt",
+ },
+ ]
+ }
+ """;
+
+ // Act
+ var result = _parser.ParseBookmarks(json);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(2, result.Data.Count);
+ Assert.AreEqual("Google", result.Data[0].Name);
+ Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
+ }
+
+ [TestMethod]
+ public void ParseBookmarks_JsonWithDifferentCasing_ParsesSuccessfully()
+ {
+ // Arrange - JSON with different property name casing (should be handled by PropertyNameCaseInsensitive option)
+ var json = """
+ {
+ "data": [
+ {
+ "name": "Google",
+ "bookmark": "https://www.google.com"
+ }
+ ]
+ }
+ """;
+
+ // Act
+ var result = _parser.ParseBookmarks(json);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(1, result.Data.Count);
+ Assert.AreEqual("Google", result.Data[0].Name);
+ Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
+ }
+
+ [TestMethod]
+ public void SerializeBookmarks_ValidBookmarks_ReturnsJsonString()
+ {
+ // Arrange
+ var bookmarks = new Bookmarks
+ {
+ Data = new List
+ {
+ 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
+ {
+ 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");
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarksCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarksCommandProviderTests.cs
new file mode 100644
index 0000000000..52f50727a7
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarksCommandProviderTests.cs
@@ -0,0 +1,137 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.CmdPal.Ext.Bookmarks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
+
+[TestClass]
+public class BookmarksCommandProviderTests
+{
+ [TestMethod]
+ public void ProviderHasCorrectId()
+ {
+ // Setup
+ var mockDataSource = new MockBookmarkDataSource();
+ var provider = new BookmarksCommandProvider(mockDataSource);
+
+ // Assert
+ Assert.AreEqual("Bookmarks", provider.Id);
+ }
+
+ [TestMethod]
+ public void ProviderHasDisplayName()
+ {
+ // Setup
+ var mockDataSource = new MockBookmarkDataSource();
+ var provider = new BookmarksCommandProvider(mockDataSource);
+
+ // Assert
+ Assert.IsNotNull(provider.DisplayName);
+ Assert.IsTrue(provider.DisplayName.Length > 0);
+ }
+
+ [TestMethod]
+ public void ProviderHasIcon()
+ {
+ // Setup
+ var provider = new BookmarksCommandProvider();
+
+ // Assert
+ Assert.IsNotNull(provider.Icon);
+ }
+
+ [TestMethod]
+ public void TopLevelCommandsNotEmpty()
+ {
+ // Setup
+ var provider = new BookmarksCommandProvider();
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+ Assert.IsTrue(commands.Length > 0);
+ }
+
+ [TestMethod]
+ public void ProviderWithMockData_LoadsBookmarksCorrectly()
+ {
+ // Arrange
+ var jsonData = @"{
+ ""Data"": [
+ {
+ ""Name"": ""Test Bookmark"",
+ ""Bookmark"": ""https://test.com""
+ },
+ {
+ ""Name"": ""Another Bookmark"",
+ ""Bookmark"": ""https://another.com""
+ }
+ ]
+ }";
+
+ var dataSource = new MockBookmarkDataSource(jsonData);
+ var provider = new BookmarksCommandProvider(dataSource);
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+
+ var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
+ var testBookmark = commands.Where(c => c.Title.Contains("Test Bookmark")).FirstOrDefault();
+
+ // Should have three commands:Add + two custom bookmarks
+ Assert.AreEqual(3, commands.Length);
+
+ Assert.IsNotNull(addCommand);
+ Assert.IsNotNull(testBookmark);
+ }
+
+ [TestMethod]
+ public void ProviderWithEmptyData_HasOnlyAddCommand()
+ {
+ // Arrange
+ var dataSource = new MockBookmarkDataSource(@"{ ""Data"": [] }");
+ var provider = new BookmarksCommandProvider(dataSource);
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+
+ // Only have Add command
+ Assert.AreEqual(1, commands.Length);
+
+ var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
+ Assert.IsNotNull(addCommand);
+ }
+
+ [TestMethod]
+ public void ProviderWithInvalidData_HandlesGracefully()
+ {
+ // Arrange
+ var dataSource = new MockBookmarkDataSource("invalid json");
+ var provider = new BookmarksCommandProvider(dataSource);
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+
+ // Only have one command. Will ignore json parse error.
+ Assert.AreEqual(1, commands.Length);
+
+ var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
+ Assert.IsNotNull(addCommand);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj
new file mode 100644
index 0000000000..07b6a9bfe5
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+ false
+ true
+ Microsoft.CmdPal.Ext.Bookmarks.UnitTests
+ $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs
new file mode 100644
index 0000000000..ae3732559c
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
+
+internal sealed class MockBookmarkDataSource : IBookmarkDataSource
+{
+ private string _jsonData;
+
+ public MockBookmarkDataSource(string initialJsonData = "[]")
+ {
+ _jsonData = initialJsonData;
+ }
+
+ public string GetBookmarkData()
+ {
+ return _jsonData;
+ }
+
+ public void SaveBookmarkData(string jsonData)
+ {
+ _jsonData = jsonData;
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/QueryTests.cs
new file mode 100644
index 0000000000..767460fa27
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/QueryTests.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using Microsoft.CmdPal.Ext.UnitTestBase;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
+
+[TestClass]
+public class QueryTests : CommandPaletteUnitTestBase
+{
+ [TestMethod]
+ public void ValidateBookmarksCreation()
+ {
+ // Setup
+ var bookmarks = Settings.CreateDefaultBookmarks();
+
+ // Assert
+ Assert.IsNotNull(bookmarks);
+ Assert.IsNotNull(bookmarks.Data);
+ Assert.AreEqual(2, bookmarks.Data.Count);
+ }
+
+ [TestMethod]
+ public void ValidateBookmarkData()
+ {
+ // Setup
+ var bookmarks = Settings.CreateDefaultBookmarks();
+
+ // Act
+ var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
+ var githubBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "GitHub");
+
+ // Assert
+ Assert.IsNotNull(microsoftBookmark);
+ Assert.AreEqual("https://www.microsoft.com", microsoftBookmark.Bookmark);
+
+ Assert.IsNotNull(githubBookmark);
+ Assert.AreEqual("https://github.com", githubBookmark.Bookmark);
+ }
+
+ [TestMethod]
+ public void ValidateWebUrlDetection()
+ {
+ // Setup
+ var bookmarks = Settings.CreateDefaultBookmarks();
+ var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
+
+ // Assert
+ Assert.IsNotNull(microsoftBookmark);
+ Assert.IsTrue(microsoftBookmark.IsWebUrl());
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Settings.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Settings.cs
new file mode 100644
index 0000000000..82d7cd1cad
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Settings.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
+
+public static class Settings
+{
+ public static Bookmarks CreateDefaultBookmarks()
+ {
+ var bookmarks = new Bookmarks();
+
+ // Add some test bookmarks
+ bookmarks.Data.Add(new BookmarkData
+ {
+ Name = "Microsoft",
+ Bookmark = "https://www.microsoft.com",
+ });
+
+ bookmarks.Data.Add(new BookmarkData
+ {
+ Name = "GitHub",
+ Bookmark = "https://github.com",
+ });
+
+ return bookmarks;
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs
new file mode 100644
index 0000000000..5c4cf39783
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs
@@ -0,0 +1,60 @@
+// 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
-internal sealed partial class PackageRepository : ListRepository, IProgramRepository
+internal sealed partial class PackageRepository : ListRepository, IProgramRepository
{
private readonly IPackageCatalog _packageCatalog;
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs
new file mode 100644
index 0000000000..7cc82c9c02
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.Bookmarks;
+
+public class BookmarkJsonParser
+{
+ public BookmarkJsonParser()
+ {
+ }
+
+ public Bookmarks ParseBookmarks(string json)
+ {
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return new Bookmarks();
+ }
+
+ try
+ {
+ var bookmarks = JsonSerializer.Deserialize(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);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
index 8f2e257782..b02eb54e0f 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
@@ -11,34 +11,4 @@ namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed class Bookmarks
{
public List 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(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);
- }
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs
index 081fb2bccb..1174685729 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs
@@ -20,10 +20,20 @@ public partial class BookmarksCommandProvider : CommandProvider
private readonly AddBookmarkPage _addNewCommand = new(null);
+ private readonly IBookmarkDataSource _dataSource;
+ private readonly BookmarkJsonParser _parser;
private Bookmarks? _bookmarks;
public BookmarksCommandProvider()
+ : this(new FileBookmarkDataSource(StateJsonPath()))
{
+ }
+
+ internal BookmarksCommandProvider(IBookmarkDataSource dataSource)
+ {
+ _dataSource = dataSource;
+ _parser = new BookmarkJsonParser();
+
Id = "Bookmarks";
DisplayName = Resources.bookmarks_display_name;
Icon = Icons.PinIcon;
@@ -49,10 +59,14 @@ public partial class BookmarksCommandProvider : CommandProvider
private void SaveAndUpdateCommands()
{
- if (_bookmarks is not null)
+ try
{
- var jsonPath = BookmarksCommandProvider.StateJsonPath();
- Bookmarks.WriteToFile(jsonPath, _bookmarks);
+ var jsonData = _parser.SerializeBookmarks(_bookmarks);
+ _dataSource.SaveBookmarkData(jsonData);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to save bookmarks: {ex.Message}");
}
LoadCommands();
@@ -82,11 +96,8 @@ public partial class BookmarksCommandProvider : CommandProvider
{
try
{
- var jsonFile = StateJsonPath();
- if (File.Exists(jsonFile))
- {
- _bookmarks = Bookmarks.ReadFromFile(jsonFile);
- }
+ var jsonData = _dataSource.GetBookmarkData();
+ _bookmarks = _parser.ParseBookmarks(jsonData);
}
catch (Exception ex)
{
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs
new file mode 100644
index 0000000000..a87859c3ce
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.Bookmarks;
+
+public class FileBookmarkDataSource : IBookmarkDataSource
+{
+ private readonly string _filePath;
+
+ public FileBookmarkDataSource(string filePath)
+ {
+ _filePath = filePath;
+ }
+
+ public string GetBookmarkData()
+ {
+ if (!File.Exists(_filePath))
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ return File.ReadAllText(_filePath);
+ }
+ catch (Exception ex)
+ {
+ ExtensionHost.LogMessage($"Read bookmark data failed. ex: {ex.Message}");
+ return string.Empty;
+ }
+ }
+
+ public void SaveBookmarkData(string jsonData)
+ {
+ try
+ {
+ File.WriteAllText(_filePath, jsonData);
+ }
+ catch (Exception ex)
+ {
+ ExtensionHost.LogMessage($"Failed to save bookmark data: {ex}");
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs
new file mode 100644
index 0000000000..7ed936a1c7
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+namespace Microsoft.CmdPal.Ext.Bookmarks;
+
+public interface IBookmarkDataSource
+{
+ string GetBookmarkData();
+
+ void SaveBookmarkData(string jsonData);
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..a74d97eeca
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Bookmarks.UnitTests")]
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs
index 4ca772dc1e..f41f5e0ab7 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Commands/ExecuteItem.cs
@@ -14,14 +14,14 @@ namespace Microsoft.CmdPal.Ext.Shell.Commands;
internal sealed partial class ExecuteItem : InvokableCommand
{
- private readonly SettingsManager _settings;
+ private readonly ISettingsInterface _settings;
private readonly RunAsType _runas;
public string Cmd { get; internal set; } = string.Empty;
private static readonly char[] Separator = [' '];
- public ExecuteItem(string cmd, SettingsManager settings, RunAsType type = RunAsType.None)
+ public ExecuteItem(string cmd, ISettingsInterface settings, RunAsType type = RunAsType.None)
{
if (type == RunAsType.Administrator)
{
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ISettingsInterface.cs
new file mode 100644
index 0000000000..4a03d55d3d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ISettingsInterface.cs
@@ -0,0 +1,20 @@
+// 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.Shell.Helpers;
+
+public interface ISettingsInterface
+{
+ public bool LeaveShellOpen { get; }
+
+ public string ShellCommandExecution { get; }
+
+ public bool RunAsAdministrator { get; }
+
+ public Dictionary Count { get; }
+
+ public void AddCmdHistory(string cmdName);
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs
index a39e723338..9d58bc939d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/SettingsManager.cs
@@ -9,7 +9,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
-public class SettingsManager : JsonSettingsManager
+public class SettingsManager : JsonSettingsManager, ISettingsInterface
{
private static readonly string _namespace = "shell";
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs
index 6a545c7225..eed1d71e49 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs
@@ -17,9 +17,9 @@ namespace Microsoft.CmdPal.Ext.Shell.Helpers;
public class ShellListPageHelpers
{
private static readonly CompositeFormat CmdHasBeenExecutedTimes = System.Text.CompositeFormat.Parse(Properties.Resources.cmd_has_been_executed_times);
- private readonly SettingsManager _settings;
+ private readonly ISettingsInterface _settings;
- public ShellListPageHelpers(SettingsManager settings)
+ public ShellListPageHelpers(ISettingsInterface settings)
{
_settings = settings;
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs
index 4b99477d2a..fde17ba14c 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs
@@ -35,7 +35,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
private bool _loadedInitialHistory;
- public ShellListPage(SettingsManager settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
+ public ShellListPage(ISettingsInterface settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
{
Icon = Icons.RunV2Icon;
Id = "com.microsoft.cmdpal.shell";
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..e308b0e6cc
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Shell.UnitTests")]
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs
index 1004f151a3..ad9b43f859 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs
@@ -13,11 +13,11 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class SearchWebCommand : InvokableCommand
{
- private readonly SettingsManager _settingsManager;
+ private readonly ISettingsInterface _settingsManager;
public string Arguments { get; internal set; } = string.Empty;
- internal SearchWebCommand(string arguments, SettingsManager settingsManager)
+ internal SearchWebCommand(string arguments, ISettingsInterface settingsManager)
{
Arguments = arguments;
BrowserInfo.UpdateIfTimePassed();
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/ISettingsInterface.cs
new file mode 100644
index 0000000000..c9a5723c15
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/ISettingsInterface.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
+
+public interface ISettingsInterface
+{
+ public bool GlobalIfURI { get; }
+
+ public string ShowHistory { get; }
+
+ public List LoadHistory();
+
+ public void SaveHistory(HistoryItem historyItem);
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
index 300cb105fb..31bbdae697 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
@@ -14,7 +14,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
-public class SettingsManager : JsonSettingsManager
+public class SettingsManager : JsonSettingsManager, ISettingsInterface
{
private readonly string _historyPath;
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
index faf65cd973..6814e83ddb 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
@@ -20,12 +20,12 @@ internal sealed partial class WebSearchListPage : DynamicListPage
{
private readonly string _iconPath = string.Empty;
private readonly List? _historyItems;
- private readonly SettingsManager _settingsManager;
+ private readonly ISettingsInterface _settingsManager;
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private List _allItems;
- public WebSearchListPage(SettingsManager settingsManager)
+ public WebSearchListPage(ISettingsInterface settingsManager)
{
Name = Resources.command_item_title;
Title = Resources.command_item_title;
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..b66aababe0
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.WebSearch.UnitTests")]
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs
index 6e874b1581..feb7aac9c0 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Helpers/ServiceHelper.cs
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WindowsServices.Helpers;
public static class ServiceHelper
{
- public static IEnumerable Search(string search)
+ public static IEnumerable Search(string search, string filterId)
{
var services = ServiceController.GetServices().OrderBy(s => s.DisplayName);
IEnumerable serviceList = [];
@@ -44,6 +44,21 @@ public static class ServiceHelper
serviceList = servicesStartsWith.Concat(servicesContains);
}
+ switch (filterId)
+ {
+ case "running":
+ serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Running);
+ break;
+ case "stopped":
+ serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Stopped);
+ break;
+ case "paused":
+ serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Paused);
+ break;
+ case "all":
+ break;
+ }
+
var result = serviceList.Select(s =>
{
var serviceResult = ServiceResult.CreateServiceController(s);
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServiceFilters.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServiceFilters.cs
new file mode 100644
index 0000000000..179315b0c3
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServiceFilters.cs
@@ -0,0 +1,26 @@
+// 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.WindowsServices;
+
+namespace Microsoft.CommandPalette.Extensions.Toolkit;
+
+public partial class ServiceFilters : Filters
+{
+ public ServiceFilters()
+ {
+ CurrentFilterId = "all";
+ }
+
+ public override IFilterItem[] GetFilters()
+ {
+ return [
+ new Filter() { Id = "all", Name = "All Services" },
+ new Separator(),
+ new Filter() { Id = "running", Name = "Running", Icon = Icons.GreenCircleIcon },
+ new Filter() { Id = "stopped", Name = "Stopped", Icon = Icons.RedCircleIcon },
+ new Filter() { Id = "paused", Name = "Paused", Icon = Icons.PauseIcon },
+ ];
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs
index 1f361b6b10..4892a1594b 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Pages/ServicesListPage.cs
@@ -16,13 +16,19 @@ internal sealed partial class ServicesListPage : DynamicListPage
{
Icon = Icons.ServicesIcon;
Name = "Windows Services";
+
+ var filters = new ServiceFilters();
+ filters.PropChanged += Filters_PropChanged;
+ Filters = filters;
}
+ private void Filters_PropChanged(object sender, IPropChangedEventArgs args) => RaiseItemsChanged();
+
public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(0);
public override IListItem[] GetItems()
{
- var items = ServiceHelper.Search(SearchText).ToArray();
+ var items = ServiceHelper.Search(SearchText, Filters.CurrentFilterId).ToArray();
return items;
}
diff --git a/src/modules/cmdpal/ext/ProcessMonitorExtension/ProcessMonitorExtension.csproj b/src/modules/cmdpal/ext/ProcessMonitorExtension/ProcessMonitorExtension.csproj
index d36c277705..8ec263d9bb 100644
--- a/src/modules/cmdpal/ext/ProcessMonitorExtension/ProcessMonitorExtension.csproj
+++ b/src/modules/cmdpal/ext/ProcessMonitorExtension/ProcessMonitorExtension.csproj
@@ -31,6 +31,10 @@
+
+
+
+
diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj b/src/modules/powerrename/dll/PowerRenameExt.vcxproj
index 32484c75b6..9c612a08ff 100644
--- a/src/modules/powerrename/dll/PowerRenameExt.vcxproj
+++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj
@@ -34,6 +34,7 @@
+
diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters b/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters
index ffd2177829..f4f0ffcbe5 100644
--- a/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters
+++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters
@@ -36,6 +36,9 @@
Header Files
+
+ Header Files
+
diff --git a/src/modules/powerrename/dll/RuntimeRegistration.h b/src/modules/powerrename/dll/RuntimeRegistration.h
new file mode 100644
index 0000000000..3cb06d5876
--- /dev/null
+++ b/src/modules/powerrename/dll/RuntimeRegistration.h
@@ -0,0 +1,37 @@
+// Header-only runtime registration for PowerRename context menu extension.
+#pragma once
+
+#include
+
+// Provided by dllmain.cpp
+extern HINSTANCE g_hInst;
+
+namespace PowerRenameRuntimeRegistration
+{
+ namespace
+ {
+ inline runtime_shell_ext::Spec BuildSpec()
+ {
+ runtime_shell_ext::Spec spec;
+ spec.clsid = L"{0440049F-D1DC-4E46-B27B-98393D79486B}";
+ spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\PowerRename";
+ spec.sentinelValue = L"ContextMenuRegistered";
+ spec.dllFileCandidates = { L"PowerToys.PowerRenameExt.dll" };
+ spec.contextMenuHandlerKeyPaths = {
+ L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\PowerRenameExt",
+ L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\PowerRenameExt" };
+ spec.friendlyName = L"PowerRename Shell Extension";
+ return spec;
+ }
+ }
+
+ inline bool EnsureRegistered()
+ {
+ return runtime_shell_ext::EnsureRegistered(BuildSpec(), g_hInst);
+ }
+
+ inline void Unregister()
+ {
+ runtime_shell_ext::Unregister(BuildSpec());
+ }
+}
diff --git a/src/modules/powerrename/dll/dllmain.cpp b/src/modules/powerrename/dll/dllmain.cpp
index 4f9c918fb6..18c612a304 100644
--- a/src/modules/powerrename/dll/dllmain.cpp
+++ b/src/modules/powerrename/dll/dllmain.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include "RuntimeRegistration.h"
#include
@@ -196,12 +197,17 @@ public:
{
std::wstring path = get_module_folderpath(g_hInst);
std::wstring packageUri = path + L"\\PowerRenameContextMenuPackage.msix";
-
if (!package::IsPackageRegisteredWithPowerToysVersion(PowerRenameConstants::ModulePackageDisplayName))
{
package::RegisterSparsePackage(path, packageUri);
}
}
+ else
+ {
+#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
+ PowerRenameRuntimeRegistration::EnsureRegistered();
+#endif
+ }
}
// Disable the powertoy
@@ -209,6 +215,13 @@ public:
{
m_enabled = false;
Logger::info(L"PowerRename disabled");
+ if (!package::IsWin11OrGreater())
+ {
+#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
+ PowerRenameRuntimeRegistration::Unregister();
+ Logger::info(L"PowerRename context menu unregistered (Win10)");
+#endif
+ }
}
// Returns if the powertoy is enabled
diff --git a/src/modules/previewpane/powerpreview/CLSID.h b/src/modules/previewpane/powerpreview/CLSID.h
index 4c866a6b80..0c9aeee0df 100644
--- a/src/modules/previewpane/powerpreview/CLSID.h
+++ b/src/modules/previewpane/powerpreview/CLSID.h
@@ -66,6 +66,7 @@ const std::vector> 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 }
diff --git a/src/runner/Resources.resx b/src/runner/Resources.resx
index 3cc2f1ad36..c8eb5f25cc 100644
--- a/src/runner/Resources.resx
+++ b/src/runner/Resources.resx
@@ -176,10 +176,6 @@
Documentation
-
- Exit
- Exit as a verb, as in Exit the application
-
Report bug
@@ -193,4 +189,8 @@
Administrator
+
+ Close
+ Close as a verb, as in Close the application
+
\ No newline at end of file
diff --git a/src/runner/centralized_hotkeys.h b/src/runner/centralized_hotkeys.h
index 29ef079f9e..bb503d332d 100644
--- a/src/runner/centralized_hotkeys.h
+++ b/src/runner/centralized_hotkeys.h
@@ -20,11 +20,13 @@ namespace CentralizedHotkeys
{
WORD modifiersMask;
WORD vkCode;
+ int hotkeyID;
- Shortcut(WORD modifiersMask = 0, WORD vkCode = 0)
+ Shortcut(WORD modifiersMask = 0, WORD vkCode = 0, const int hotkeyID = 0)
{
this->modifiersMask = modifiersMask;
this->vkCode = vkCode;
+ this->hotkeyID = hotkeyID;
}
bool operator<(const Shortcut& key) const
diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp
index 4fdf6b74d2..bb45f7f5ae 100644
--- a/src/runner/general_settings.cpp
+++ b/src/runner/general_settings.cpp
@@ -3,6 +3,7 @@
#include "auto_start_helper.h"
#include "tray_icon.h"
#include "Generated files/resource.h"
+#include "hotkey_conflict_detector.h"
#include
#include "powertoy_module.h"
@@ -204,11 +205,15 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
{
Logger::info(L"apply_general_settings: Enabling powertoy {}", name);
powertoy->enable();
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ hkmng.EnableHotkeyByModule(name);
}
else
{
Logger::info(L"apply_general_settings: Disabling powertoy {}", name);
powertoy->disable();
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ hkmng.DisableHotkeyByModule(name);
}
// Sync the hotkey state with the module state, so it can be removed for disabled modules.
powertoy.UpdateHotkeyEx();
@@ -315,6 +320,8 @@ void start_enabled_powertoys()
{
Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name);
powertoy->enable();
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ hkmng.EnableHotkeyByModule(name);
powertoy.UpdateHotkeyEx();
}
}
diff --git a/src/runner/hotkey_conflict_detector.cpp b/src/runner/hotkey_conflict_detector.cpp
new file mode 100644
index 0000000000..14c8a1ecd9
--- /dev/null
+++ b/src/runner/hotkey_conflict_detector.cpp
@@ -0,0 +1,471 @@
+#include "pch.h"
+#include "hotkey_conflict_detector.h"
+#include
+#include
+#include
+#include
+
+namespace HotkeyConflictDetector
+{
+ Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut)
+ {
+ Hotkey hotkey;
+
+ hotkey.win = (shortcut.modifiersMask & MOD_WIN) != 0;
+ hotkey.ctrl = (shortcut.modifiersMask & MOD_CONTROL) != 0;
+ hotkey.shift = (shortcut.modifiersMask & MOD_SHIFT) != 0;
+ hotkey.alt = (shortcut.modifiersMask & MOD_ALT) != 0;
+
+ hotkey.key = shortcut.vkCode > 255 ? 0 : static_cast(shortcut.vkCode);
+
+ return hotkey;
+ }
+
+ HotkeyConflictManager* HotkeyConflictManager::instance = nullptr;
+ std::mutex HotkeyConflictManager::instanceMutex;
+
+ HotkeyConflictManager& HotkeyConflictManager::GetInstance()
+ {
+ std::lock_guard lock(instanceMutex);
+ if (instance == nullptr)
+ {
+ instance = new HotkeyConflictManager();
+ }
+ return *instance;
+ }
+
+ HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID)
+ {
+ if (disabledHotkeys.find(_moduleName) != disabledHotkeys.end())
+ {
+ return HotkeyConflictType::NoConflict;
+ }
+
+ uint16_t handle = GetHotkeyHandle(_hotkey);
+
+ if (handle == 0)
+ {
+ return HotkeyConflictType::NoConflict;
+ }
+
+ // The order is important, first to check sys conflict and then inapp conflict
+ if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
+ {
+ return HotkeyConflictType::SystemConflict;
+ }
+
+ if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
+ {
+ return HotkeyConflictType::InAppConflict;
+ }
+
+ auto it = hotkeyMap.find(handle);
+
+ if (it == hotkeyMap.end())
+ {
+ return HasConflictWithSystemHotkey(_hotkey) ?
+ HotkeyConflictType::SystemConflict :
+ HotkeyConflictType::NoConflict;
+ }
+
+ if (wcscmp(it->second.moduleName.c_str(), _moduleName) == 0 && it->second.hotkeyID == _hotkeyID)
+ {
+ // A shortcut matching its own assignment is not considered a conflict.
+ return HotkeyConflictType::NoConflict;
+ }
+
+ return HotkeyConflictType::InAppConflict;
+ }
+
+ HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey)
+ {
+ uint16_t handle = GetHotkeyHandle(_hotkey);
+
+ if (handle == 0)
+ {
+ return HotkeyConflictType::NoConflict;
+ }
+
+ // The order is important, first to check sys conflict and then inapp conflict
+ if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
+ {
+ return HotkeyConflictType::SystemConflict;
+ }
+
+ if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
+ {
+ return HotkeyConflictType::InAppConflict;
+ }
+
+ auto it = hotkeyMap.find(handle);
+
+ if (it == hotkeyMap.end())
+ {
+ return HasConflictWithSystemHotkey(_hotkey) ?
+ HotkeyConflictType::SystemConflict :
+ HotkeyConflictType::NoConflict;
+ }
+
+ return HotkeyConflictType::InAppConflict;
+ }
+
+ // This function should only be called when a conflict has already been identified.
+ // It returns a list of all conflicting shortcuts.
+ std::vector HotkeyConflictManager::GetAllConflicts(Hotkey const& _hotkey)
+ {
+ std::vector conflicts;
+ uint16_t handle = GetHotkeyHandle(_hotkey);
+
+ // Check in-app conflicts first
+ auto inAppIt = inAppConflictHotkeyMap.find(handle);
+ if (inAppIt != inAppConflictHotkeyMap.end())
+ {
+ // Add all in-app conflicts
+ for (const auto& conflict : inAppIt->second)
+ {
+ conflicts.push_back(conflict);
+ }
+
+ return conflicts;
+ }
+
+ // Check system conflicts
+ auto sysIt = sysConflictHotkeyMap.find(handle);
+ if (sysIt != sysConflictHotkeyMap.end())
+ {
+ HotkeyConflictInfo systemConflict;
+ systemConflict.hotkey = _hotkey;
+ systemConflict.moduleName = L"System";
+ systemConflict.hotkeyID = 0;
+
+ conflicts.push_back(systemConflict);
+
+ return conflicts;
+ }
+
+ // Check if there's a successfully registered hotkey that would conflict
+ auto registeredIt = hotkeyMap.find(handle);
+ if (registeredIt != hotkeyMap.end())
+ {
+ conflicts.push_back(registeredIt->second);
+
+ return conflicts;
+ }
+
+ // If all the above conditions are ruled out, a system-level conflict is the only remaining explanation.
+ HotkeyConflictInfo systemConflict;
+ systemConflict.hotkey = _hotkey;
+ systemConflict.moduleName = L"System";
+ systemConflict.hotkeyID = 0;
+ conflicts.push_back(systemConflict);
+
+ return conflicts;
+ }
+
+ bool HotkeyConflictManager::AddHotkey(Hotkey const& _hotkey, const wchar_t* _moduleName, const int _hotkeyID, bool isEnabled)
+ {
+ if (!isEnabled)
+ {
+ disabledHotkeys[_moduleName].push_back({ _hotkey, _moduleName, _hotkeyID });
+ return true;
+ }
+
+ uint16_t handle = GetHotkeyHandle(_hotkey);
+
+ if (handle == 0)
+ {
+ return false;
+ }
+
+ HotkeyConflictType conflictType = HasConflict(_hotkey, _moduleName, _hotkeyID);
+ if (conflictType != HotkeyConflictType::NoConflict)
+ {
+ if (conflictType == HotkeyConflictType::InAppConflict)
+ {
+ auto hotkeyFound = hotkeyMap.find(handle);
+ inAppConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID });
+
+ if (hotkeyFound != hotkeyMap.end())
+ {
+ inAppConflictHotkeyMap[handle].insert(hotkeyFound->second);
+ hotkeyMap.erase(hotkeyFound);
+ }
+ }
+ else
+ {
+ sysConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyID });
+ }
+ return false;
+ }
+
+ HotkeyConflictInfo hotkeyInfo;
+ hotkeyInfo.moduleName = _moduleName;
+ hotkeyInfo.hotkeyID = _hotkeyID;
+ hotkeyInfo.hotkey = _hotkey;
+ hotkeyMap[handle] = hotkeyInfo;
+
+ return true;
+ }
+
+ std::vector HotkeyConflictManager::RemoveHotkeyByModule(const std::wstring& moduleName)
+ {
+ std::vector removedHotkeys;
+
+ if (disabledHotkeys.find(moduleName) != disabledHotkeys.end())
+ {
+ disabledHotkeys.erase(moduleName);
+ }
+
+ std::lock_guard lock(hotkeyMutex);
+ bool foundRecord = false;
+
+ for (auto it = sysConflictHotkeyMap.begin(); it != sysConflictHotkeyMap.end();)
+ {
+ auto& conflictSet = it->second;
+ for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
+ {
+ if (setIt->moduleName == moduleName)
+ {
+ removedHotkeys.push_back(*setIt);
+ setIt = conflictSet.erase(setIt);
+ foundRecord = true;
+ }
+ else
+ {
+ ++setIt;
+ }
+ }
+ if (conflictSet.empty())
+ {
+ it = sysConflictHotkeyMap.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ for (auto it = inAppConflictHotkeyMap.begin(); it != inAppConflictHotkeyMap.end();)
+ {
+ auto& conflictSet = it->second;
+ uint16_t handle = it->first;
+
+ for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
+ {
+ if (setIt->moduleName == moduleName)
+ {
+ removedHotkeys.push_back(*setIt);
+ setIt = conflictSet.erase(setIt);
+ foundRecord = true;
+ }
+ else
+ {
+ ++setIt;
+ }
+ }
+
+ if (conflictSet.empty())
+ {
+ it = inAppConflictHotkeyMap.erase(it);
+ }
+ else if (conflictSet.size() == 1)
+ {
+ // Move the only remaining conflict to main map
+ const auto& onlyConflict = *conflictSet.begin();
+ hotkeyMap[handle] = onlyConflict;
+ it = inAppConflictHotkeyMap.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ for (auto it = hotkeyMap.begin(); it != hotkeyMap.end();)
+ {
+ if (it->second.moduleName == moduleName)
+ {
+ uint16_t handle = it->first;
+ removedHotkeys.push_back(it->second);
+ it = hotkeyMap.erase(it);
+ foundRecord = true;
+
+ auto inAppIt = inAppConflictHotkeyMap.find(handle);
+ if (inAppIt != inAppConflictHotkeyMap.end() && inAppIt->second.size() == 1)
+ {
+ // Move the only in-app conflict to main map
+ const auto& onlyConflict = *inAppIt->second.begin();
+ hotkeyMap[handle] = onlyConflict;
+ inAppConflictHotkeyMap.erase(inAppIt);
+ }
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ return removedHotkeys;
+ }
+
+ void HotkeyConflictManager::EnableHotkeyByModule(const std::wstring& moduleName)
+ {
+ if (disabledHotkeys.find(moduleName) == disabledHotkeys.end())
+ {
+ return; // No disabled hotkeys for this module
+ }
+
+ auto hotkeys = disabledHotkeys[moduleName];
+ disabledHotkeys.erase(moduleName);
+
+ for (const auto& hotkeyInfo : hotkeys)
+ {
+ // Re-add the hotkey as enabled
+ AddHotkey(hotkeyInfo.hotkey, moduleName.c_str(), hotkeyInfo.hotkeyID, true);
+ }
+ }
+
+ void HotkeyConflictManager::DisableHotkeyByModule(const std::wstring& moduleName)
+ {
+ auto hotkeys = RemoveHotkeyByModule(moduleName);
+ disabledHotkeys[moduleName] = hotkeys;
+ }
+
+ bool HotkeyConflictManager::HasConflictWithSystemHotkey(const Hotkey& hotkey)
+ {
+ // Convert PowerToys Hotkey format to Win32 RegisterHotKey format
+ UINT modifiers = 0;
+ if (hotkey.win)
+ {
+ modifiers |= MOD_WIN;
+ }
+ if (hotkey.ctrl)
+ {
+ modifiers |= MOD_CONTROL;
+ }
+ if (hotkey.alt)
+ {
+ modifiers |= MOD_ALT;
+ }
+ if (hotkey.shift)
+ {
+ modifiers |= MOD_SHIFT;
+ }
+
+ // No modifiers or no key is not a valid hotkey
+ if (modifiers == 0 || hotkey.key == 0)
+ {
+ return false;
+ }
+
+ // Use a unique ID for this test registration
+ const int hotkeyId = 0x0FFF; // Arbitrary ID for temporary registration
+
+ // Try to register the hotkey with Windows, using nullptr instead of a window handle
+ if (!RegisterHotKey(nullptr, hotkeyId, modifiers, hotkey.key))
+ {
+ // If registration fails with ERROR_HOTKEY_ALREADY_REGISTERED, it means the hotkey
+ // is already in use by the system or another application
+ if (GetLastError() == ERROR_HOTKEY_ALREADY_REGISTERED)
+ {
+ return true;
+ }
+ }
+ else
+ {
+ // If registration succeeds, unregister it immediately
+ UnregisterHotKey(nullptr, hotkeyId);
+ }
+
+ return false;
+ }
+
+ json::JsonObject HotkeyConflictManager::GetHotkeyConflictsAsJson()
+ {
+ std::lock_guard lock(hotkeyMutex);
+
+ using namespace json;
+ JsonObject root;
+
+ // Serialize hotkey to a unique string format for grouping
+ auto serializeHotkey = [](const Hotkey& hotkey) -> JsonObject {
+ JsonObject obj;
+ obj.Insert(L"win", value(hotkey.win));
+ obj.Insert(L"ctrl", value(hotkey.ctrl));
+ obj.Insert(L"shift", value(hotkey.shift));
+ obj.Insert(L"alt", value(hotkey.alt));
+ obj.Insert(L"key", value(static_cast(hotkey.key)));
+ return obj;
+ };
+
+ // New format: Group conflicts by hotkey
+ JsonArray inAppConflictsArray;
+ JsonArray sysConflictsArray;
+
+ // Process in-app conflicts - only include hotkeys that are actually in conflict
+ for (const auto& [handle, conflicts] : inAppConflictHotkeyMap)
+ {
+ if (!conflicts.empty())
+ {
+ JsonObject conflictGroup;
+
+ // All entries have the same hotkey, so use the first one for the key
+ conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
+
+ // Create an array of module info without repeating the hotkey
+ JsonArray modules;
+ for (const auto& info : conflicts)
+ {
+ JsonObject moduleInfo;
+ moduleInfo.Insert(L"moduleName", value(info.moduleName));
+ moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID));
+ modules.Append(moduleInfo);
+ }
+
+ conflictGroup.Insert(L"modules", modules);
+ inAppConflictsArray.Append(conflictGroup);
+ }
+ }
+
+ // Process system conflicts - only include hotkeys that are actually in conflict
+ for (const auto& [handle, conflicts] : sysConflictHotkeyMap)
+ {
+ if (!conflicts.empty())
+ {
+ JsonObject conflictGroup;
+
+ // All entries have the same hotkey, so use the first one for the key
+ conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
+
+ // Create an array of module info without repeating the hotkey
+ JsonArray modules;
+ for (const auto& info : conflicts)
+ {
+ JsonObject moduleInfo;
+ moduleInfo.Insert(L"moduleName", value(info.moduleName));
+ moduleInfo.Insert(L"hotkeyID", value(info.hotkeyID));
+ modules.Append(moduleInfo);
+ }
+
+ conflictGroup.Insert(L"modules", modules);
+ sysConflictsArray.Append(conflictGroup);
+ }
+ }
+
+ // Add the grouped conflicts to the root object
+ root.Insert(L"inAppConflicts", inAppConflictsArray);
+ root.Insert(L"sysConflicts", sysConflictsArray);
+
+ return root;
+ }
+
+ uint16_t HotkeyConflictManager::GetHotkeyHandle(const Hotkey& hotkey)
+ {
+ uint16_t handle = hotkey.key;
+ handle |= hotkey.win << 8;
+ handle |= hotkey.ctrl << 9;
+ handle |= hotkey.shift << 10;
+ handle |= hotkey.alt << 11;
+ return handle;
+ }
+}
\ No newline at end of file
diff --git a/src/runner/hotkey_conflict_detector.h b/src/runner/hotkey_conflict_detector.h
new file mode 100644
index 0000000000..c32954e3e4
--- /dev/null
+++ b/src/runner/hotkey_conflict_detector.h
@@ -0,0 +1,100 @@
+#pragma once
+#include "pch.h"
+#include
+#include
+#include
+
+#include "../modules/interface/powertoy_module_interface.h"
+#include "centralized_hotkeys.h"
+#include "common/utils/json.h"
+
+namespace HotkeyConflictDetector
+{
+ using Hotkey = PowertoyModuleIface::Hotkey;
+ using HotkeyEx = PowertoyModuleIface::HotkeyEx;
+ using Shortcut = CentralizedHotkeys::Shortcut;
+
+ struct HotkeyConflictInfo
+ {
+ Hotkey hotkey;
+ std::wstring moduleName;
+ int hotkeyID = 0;
+
+ inline bool operator==(const HotkeyConflictInfo& other) const
+ {
+ return hotkey == other.hotkey &&
+ moduleName == other.moduleName &&
+ hotkeyID == other.hotkeyID;
+ }
+ };
+
+ Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut);
+
+ enum HotkeyConflictType
+ {
+ NoConflict = 0,
+ SystemConflict = 1,
+ InAppConflict = 2,
+ };
+
+ class HotkeyConflictManager
+ {
+ public:
+ static HotkeyConflictManager& GetInstance();
+
+ HotkeyConflictType HasConflict(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID);
+ HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey);
+ std::vector HotkeyConflictManager::GetAllConflicts(Hotkey const& hotkey);
+ bool AddHotkey(const Hotkey& hotkey, const wchar_t* moduleName, const int hotkeyID, bool isEnabled);
+ std::vector RemoveHotkeyByModule(const std::wstring& moduleName);
+
+ void EnableHotkeyByModule(const std::wstring& moduleName);
+ void DisableHotkeyByModule(const std::wstring& moduleName);
+
+ json::JsonObject GetHotkeyConflictsAsJson();
+
+ private:
+ static std::mutex instanceMutex;
+ static HotkeyConflictManager* instance;
+
+ std::mutex hotkeyMutex;
+ // Hotkey in hotkeyMap means the hotkey has been registered successfully
+ std::unordered_map hotkeyMap;
+ // Hotkey in sysConflictHotkeyMap means the hotkey has conflict with system defined hotkeys
+ std::unordered_map> sysConflictHotkeyMap;
+ // Hotkey in inAppConflictHotkeyMap means the hotkey has conflict with other modules
+ std::unordered_map> inAppConflictHotkeyMap;
+
+ std::unordered_map> disabledHotkeys;
+
+ uint16_t GetHotkeyHandle(const Hotkey&);
+ bool HasConflictWithSystemHotkey(const Hotkey&);
+
+ HotkeyConflictManager() = default;
+ };
+};
+
+namespace std
+{
+ template<>
+ struct hash
+ {
+ size_t operator()(const HotkeyConflictDetector::HotkeyConflictInfo& info) const
+ {
+
+ size_t hotkeyHash =
+ (info.hotkey.win ? 1ULL : 0ULL) |
+ ((info.hotkey.ctrl ? 1ULL : 0ULL) << 1) |
+ ((info.hotkey.shift ? 1ULL : 0ULL) << 2) |
+ ((info.hotkey.alt ? 1ULL : 0ULL) << 3) |
+ (static_cast(info.hotkey.key) << 4);
+
+ size_t moduleHash = std::hash{}(info.moduleName);
+ size_t idHash = std::hash{}(info.hotkeyID);
+
+ return hotkeyHash ^
+ ((moduleHash << 1) | (moduleHash >> (sizeof(size_t) * 8 - 1))) ^ // rotate left 1 bit
+ ((idHash << 2) | (idHash >> (sizeof(size_t) * 8 - 2))); // rotate left 2 bits
+ }
+ };
+}
diff --git a/src/runner/powertoy_module.cpp b/src/runner/powertoy_module.cpp
index 32f856f465..eb1f7c4fd7 100644
--- a/src/runner/powertoy_module.cpp
+++ b/src/runner/powertoy_module.cpp
@@ -40,13 +40,14 @@ json::JsonObject PowertoyModule::json_config() const
}
PowertoyModule::PowertoyModule(PowertoyModuleIface* pt_module, HMODULE handle) :
- handle(handle), pt_module(pt_module)
+ handle(handle), pt_module(pt_module), hkmng(HotkeyConflictDetector::HotkeyConflictManager::GetInstance())
{
if (!pt_module)
{
throw std::runtime_error("Module not initialized");
}
+ remove_hotkey_records();
update_hotkeys();
UpdateHotkeyEx();
}
@@ -63,19 +64,27 @@ void PowertoyModule::update_hotkeys()
for (size_t i = 0; i < hotkeyCount; i++)
{
- CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] {
- Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key());
- return modulePtr->on_hotkey(i);
- });
+ if (hotkeys[i].isShown)
+ {
+ hkmng.AddHotkey(hotkeys[i], pt_module->get_key(), static_cast(i), pt_module->is_enabled());
+
+ CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] {
+ Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key());
+ return modulePtr->on_hotkey(i);
+ });
+ }
}
}
void PowertoyModule::UpdateHotkeyEx()
{
CentralizedHotkeys::UnregisterHotkeysForModule(pt_module->get_key());
+
auto container = pt_module->GetHotkeyEx();
if (container.has_value() && pt_module->is_enabled())
{
+ hkmng.RemoveHotkeyByModule(pt_module->get_key());
+
auto hotkey = container.value();
auto modulePtr = pt_module.get();
auto action = [modulePtr](WORD /*modifiersMask*/, WORD /*vkCode*/) {
@@ -83,6 +92,9 @@ void PowertoyModule::UpdateHotkeyEx()
modulePtr->OnHotkeyEx();
};
+ HotkeyConflictDetector::Hotkey _hotkey = HotkeyConflictDetector::ShortcutToHotkey({ hotkey.modifiersMask, hotkey.vkCode });
+ hkmng.AddHotkey(_hotkey, pt_module->get_key(), 0, pt_module->is_enabled()); // This is the only one activation hotkey, so we use "0" as the name.
+
CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode }, { pt_module->get_key(), action });
}
diff --git a/src/runner/powertoy_module.h b/src/runner/powertoy_module.h
index 9332e5f025..9b7a9a59bd 100644
--- a/src/runner/powertoy_module.h
+++ b/src/runner/powertoy_module.h
@@ -5,6 +5,7 @@
#include
#include
#include
+#include "hotkey_conflict_detector.h"
#include
@@ -44,9 +45,17 @@ public:
void UpdateHotkeyEx();
+ inline void remove_hotkey_records()
+ {
+ hkmng.RemoveHotkeyByModule(pt_module->get_key());
+ }
+
private:
+ HotkeyConflictDetector::HotkeyConflictManager& hkmng;
std::unique_ptr handle;
std::unique_ptr pt_module;
+
+
};
PowertoyModule load_powertoy(const std::wstring_view filename);
diff --git a/src/runner/resource.base.h b/src/runner/resource.base.h
index 027f5b4281..7037f4342d 100644
--- a/src/runner/resource.base.h
+++ b/src/runner/resource.base.h
@@ -15,7 +15,7 @@
#define APPICON 101
#define ID_TRAY_MENU 102
-#define ID_EXIT_MENU_COMMAND 40001
+#define ID_CLOSE_MENU_COMMAND 40001
#define ID_SETTINGS_MENU_COMMAND 40002
#define ID_ABOUT_MENU_COMMAND 40003
#define ID_REPORT_BUG_COMMAND 40004
diff --git a/src/runner/runner.base.rc b/src/runner/runner.base.rc
index 10a4555db8..367735ade4 100644
Binary files a/src/runner/runner.base.rc and b/src/runner/runner.base.rc differ
diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj
index a55396a71a..90dafb5e45 100644
--- a/src/runner/runner.vcxproj
+++ b/src/runner/runner.vcxproj
@@ -51,6 +51,7 @@
+
Create
@@ -71,6 +72,7 @@
+
diff --git a/src/runner/runner.vcxproj.filters b/src/runner/runner.vcxproj.filters
index a91782fd24..812d7857a2 100644
--- a/src/runner/runner.vcxproj.filters
+++ b/src/runner/runner.vcxproj.filters
@@ -45,6 +45,9 @@
Utils
+
+ Utils
+
@@ -93,6 +96,9 @@
Utils
+
+ Utils
+
diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp
index e811ff5d65..b3ced3b858 100644
--- a/src/runner/settings_window.cpp
+++ b/src/runner/settings_window.cpp
@@ -13,6 +13,7 @@
#include "UpdateUtils.h"
#include "centralized_kb_hook.h"
#include "Generated files/resource.h"
+#include "hotkey_conflict_detector.h"
#include
#include
@@ -153,6 +154,8 @@ void send_json_config_to_module(const std::wstring& module_key, const std::wstri
if (moduleIt != modules().end())
{
moduleIt->second->set_config(settings.c_str());
+
+ moduleIt->second.remove_hotkey_records();
moduleIt->second.update_hotkeys();
moduleIt->second.UpdateHotkeyEx();
}
@@ -249,6 +252,77 @@ void dispatch_received_json(const std::wstring& json_to_parse)
const std::wstring save_file_location = PTSettingsHelper::get_root_save_folder_location() + language_filename;
json::to_file(save_file_location, j);
}
+ else if (name == L"check_hotkey_conflict")
+ {
+ try
+ {
+ PowertoyModuleIface::Hotkey hotkey;
+ hotkey.win = value.GetObjectW().GetNamedBoolean(L"win", false);
+ hotkey.ctrl = value.GetObjectW().GetNamedBoolean(L"ctrl", false);
+ hotkey.shift = value.GetObjectW().GetNamedBoolean(L"shift", false);
+ hotkey.alt = value.GetObjectW().GetNamedBoolean(L"alt", false);
+ hotkey.key = static_cast(value.GetObjectW().GetNamedNumber(L"key", 0));
+
+ std::wstring requestId = value.GetObjectW().GetNamedString(L"request_id", L"").c_str();
+
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ bool hasConflict = hkmng.HasConflict(hotkey);
+
+ json::JsonObject response;
+ response.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"hotkey_conflict_result"));
+ response.SetNamedValue(L"request_id", json::JsonValue::CreateStringValue(requestId));
+ response.SetNamedValue(L"has_conflict", json::JsonValue::CreateBooleanValue(hasConflict));
+
+ if (hasConflict)
+ {
+ auto conflicts = hkmng.GetAllConflicts(hotkey);
+ if (!conflicts.empty())
+ {
+ // Include all conflicts in the response
+ json::JsonArray allConflicts;
+ for (const auto& conflict : conflicts)
+ {
+ json::JsonObject conflictObj;
+ conflictObj.SetNamedValue(L"module", json::JsonValue::CreateStringValue(conflict.moduleName));
+ conflictObj.SetNamedValue(L"hotkeyID", json::JsonValue::CreateNumberValue(conflict.hotkeyID));
+ allConflicts.Append(conflictObj);
+ }
+ response.SetNamedValue(L"all_conflicts", allConflicts);
+ }
+ }
+
+ std::unique_lock lock{ ipc_mutex };
+ if (current_settings_ipc)
+ {
+ current_settings_ipc->send(response.Stringify().c_str());
+ }
+ }
+ catch (...)
+ {
+ Logger::error(L"Failed to process hotkey conflict check request");
+ }
+ }
+ else if (name == L"get_all_hotkey_conflicts")
+ {
+ try
+ {
+ auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
+ auto conflictsJson = hkmng.GetHotkeyConflictsAsJson();
+
+ // Add response type identifier
+ conflictsJson.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"all_hotkey_conflicts"));
+
+ std::unique_lock lock{ ipc_mutex };
+ if (current_settings_ipc)
+ {
+ current_settings_ipc->send(conflictsJson.Stringify().c_str());
+ }
+ }
+ catch (...)
+ {
+ Logger::error(L"Failed to process get all hotkey conflicts request");
+ }
+ }
}
return;
}
diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp
index 53da747539..749c921659 100644
--- a/src/runner/tray_icon.cpp
+++ b/src/runner/tray_icon.cpp
@@ -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_EXIT_MENU_COMMAND:
+ case ID_CLOSE_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 exit_menuitem_label = GET_RESOURCE_STRING(IDS_EXIT_MENU_TEXT);
+ static std::wstring close_menuitem_label = GET_RESOURCE_STRING(IDS_CLOSE_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_EXIT_MENU_COMMAND, exit_menuitem_label.data());
+ change_menu_item_text(ID_CLOSE_MENU_COMMAND, close_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));
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
index 28bed92012..1642ecf9c4 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs
@@ -12,7 +12,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction
{
private HotkeySettings _shortcut = new();
- private bool _isShown = true;
+ private bool _isShown;
+ private bool _hasConflict;
+ private string _tooltip;
[JsonPropertyName("shortcut")]
public HotkeySettings Shortcut
@@ -38,6 +40,20 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
set => Set(ref _isShown, value);
}
+ [JsonIgnore]
+ public bool HasConflict
+ {
+ get => _hasConflict;
+ set => Set(ref _hasConflict, value);
+ }
+
+ [JsonIgnore]
+ public string Tooltip
+ {
+ get => _tooltip;
+ set => Set(ref _tooltip, value);
+ }
+
[JsonIgnore]
public IEnumerable SubActions => [];
}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
index 3b1a859364..6d908c617a 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs
@@ -28,16 +28,23 @@ public sealed class AdvancedPasteAdditionalActions
public IEnumerable GetAllActions()
{
- Queue queue = new([ImageToText, PasteAsFile, Transcode]);
+ return GetAllActionsRecursive([ImageToText, PasteAsFile, Transcode]);
+ }
- while (queue.Count != 0)
+ ///
+ /// Changed to depth-first traversal to ensure ordered output
+ ///
+ /// The collection of actions to traverse
+ /// All actions returned in depth-first order
+ private static IEnumerable GetAllActionsRecursive(IEnumerable actions)
+ {
+ foreach (var action in actions)
{
- var action = queue.Dequeue();
yield return action;
- foreach (var subAction in action.SubActions)
+ foreach (var subAction in GetAllActionsRecursive(action.SubActions))
{
- queue.Enqueue(subAction);
+ yield return subAction;
}
}
}
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
index 971d24c93b..43baf89351 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
@@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Library;
@@ -20,6 +20,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private bool _canMoveUp;
private bool _canMoveDown;
private bool _isValid;
+ private bool _hasConflict;
+ private string _tooltip;
[JsonPropertyName("id")]
public int Id
@@ -65,7 +67,6 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
// We null-coalesce here rather than outside this branch as we want to raise PropertyChanged when the setter is called
// with null; the ShortcutControl depends on this.
_shortcut = value ?? new();
-
OnPropertyChanged();
}
}
@@ -99,6 +100,20 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private set => Set(ref _isValid, value);
}
+ [JsonIgnore]
+ public bool HasConflict
+ {
+ get => _hasConflict;
+ set => Set(ref _hasConflict, value);
+ }
+
+ [JsonIgnore]
+ public string Tooltip
+ {
+ get => _tooltip;
+ set => Set(ref _tooltip, value);
+ }
+
[JsonIgnore]
public IEnumerable SubActions => [];
@@ -118,6 +133,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
IsShown = other.IsShown;
CanMoveUp = other.CanMoveUp;
CanMoveDown = other.CanMoveDown;
+ HasConflict = other.HasConflict;
+ Tooltip = other.Tooltip;
}
private HotkeySettings GetShortcutClone()
diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
index e3ba7d4122..ca9cdacff6 100644
--- a/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs
@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig
+ public class AdvancedPasteSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "AdvancedPaste";
@@ -39,6 +41,64 @@ namespace Microsoft.PowerToys.Settings.UI.Library
settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName);
}
+ public ModuleType GetModuleType() => ModuleType.AdvancedPaste;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.PasteAsPlainTextShortcut,
+ value => Properties.PasteAsPlainTextShortcut = value ?? AdvancedPasteProperties.DefaultPasteAsPlainTextShortcut,
+ "PasteAsPlainText_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.AdvancedPasteUIShortcut,
+ value => Properties.AdvancedPasteUIShortcut = value ?? AdvancedPasteProperties.DefaultAdvancedPasteUIShortcut,
+ "AdvancedPasteUI_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.PasteAsMarkdownShortcut,
+ value => Properties.PasteAsMarkdownShortcut = value ?? new HotkeySettings(),
+ "PasteAsMarkdown_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.PasteAsJsonShortcut,
+ value => Properties.PasteAsJsonShortcut = value ?? new HotkeySettings(),
+ "PasteAsJson_Shortcut"),
+ };
+
+ string[] additionalActionHeaderKeys =
+ [
+ "ImageToText",
+ "PasteAsTxtFile",
+ "PasteAsPngFile",
+ "PasteAsHtmlFile",
+ "TranscodeToMp3",
+ "TranscodeToMp4",
+ ];
+ int index = 0;
+ foreach (var action in Properties.AdditionalActions.GetAllActions())
+ {
+ if (action is AdvancedPasteAdditionalAction additionalAction)
+ {
+ hotkeyAccessors.Add(new HotkeyAccessor(
+ () => additionalAction.Shortcut,
+ value => additionalAction.Shortcut = value ?? new HotkeySettings(),
+ additionalActionHeaderKeys[index]));
+ index++;
+ }
+ }
+
+ // Custom actions do not have localization header, just use the action name.
+ foreach (var customAction in Properties.CustomActions.Value)
+ {
+ hotkeyAccessors.Add(new HotkeyAccessor(
+ () => customAction.Shortcut,
+ value => customAction.Shortcut = value ?? new HotkeySettings(),
+ customAction.Name));
+ }
+
+ return hotkeyAccessors.ToArray();
+ }
+
public string GetModuleName()
=> Name;
diff --git a/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs b/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs
index 449c1c0a76..cb7e138596 100644
--- a/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class AlwaysOnTopSettings : BasePTModuleSettings, ISettingsConfig
+ public class AlwaysOnTopSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "AlwaysOnTop";
public const string ModuleVersion = "0.0.1";
@@ -28,6 +30,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.AlwaysOnTop;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.Hotkey.Value,
+ value => Properties.Hotkey.Value = value ?? AlwaysOnTopProperties.DefaultHotkeyValue,
+ "AlwaysOnTop_ActivationShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public bool UpgradeSettingsConfiguration()
{
return false;
diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs b/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs
index 641625e180..b601b75baa 100644
--- a/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/ColorPickerSettings.cs
@@ -7,14 +7,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
-
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class ColorPickerSettings : BasePTModuleSettings, ISettingsConfig
+ public class ColorPickerSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "ColorPicker";
@@ -64,6 +64,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return false;
}
+ public ModuleType GetModuleType() => ModuleType.ColorPicker;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "Activation_Shortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public static object UpgradeSettings(object oldSettingsObject)
{
ColorPickerSettingsVersion1 oldSettings = (ColorPickerSettingsVersion1)oldSettingsObject;
diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs b/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs
index 840788992d..1ddff1946f 100644
--- a/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs
+++ b/src/settings-ui/Settings.UI.Library/ColorPickerSettingsVersion1.cs
@@ -5,7 +5,6 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
-
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs
index ed6600f287..517c4e8754 100644
--- a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class CropAndLockSettings : BasePTModuleSettings, ISettingsConfig
+ public class CropAndLockSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "CropAndLock";
public const string ModuleVersion = "0.0.1";
@@ -28,6 +30,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.CropAndLock;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ReparentHotkey.Value,
+ value => Properties.ReparentHotkey.Value = value ?? CropAndLockProperties.DefaultReparentHotkeyValue,
+ "CropAndLock_ReparentActivation_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.ThumbnailHotkey.Value,
+ value => Properties.ThumbnailHotkey.Value = value ?? CropAndLockProperties.DefaultThumbnailHotkeyValue,
+ "CropAndLock_ThumbnailActivation_Shortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public bool UpgradeSettingsConfiguration()
{
return false;
diff --git a/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs b/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs
index aca45d0b01..fb00351ee2 100644
--- a/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/FindMyMouseSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig
+ public class FindMyMouseSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "FindMyMouse";
@@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.FindMyMouse;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "MouseUtils_FindMyMouse_ActivationShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
diff --git a/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs b/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs
new file mode 100644
index 0000000000..41c4d4af61
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Helpers/HotkeyAccessor.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
+{
+ public class HotkeyAccessor
+ {
+ public Func Getter { get; }
+
+ public Action Setter { get; }
+
+ public HotkeyAccessor(Func getter, Action setter, string localizationHeaderKey = "")
+ {
+ Getter = getter ?? throw new ArgumentNullException(nameof(getter));
+ Setter = setter ?? throw new ArgumentNullException(nameof(setter));
+ LocalizationHeaderKey = localizationHeaderKey;
+ }
+
+ public HotkeySettings Value
+ {
+ get => Getter();
+ set => Setter(value);
+ }
+
+ public string LocalizationHeaderKey { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs
new file mode 100644
index 0000000000..00d2145f29
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsData.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class AllHotkeyConflictsData
+ {
+ public List InAppConflicts { get; set; } = new List();
+
+ public List SystemConflicts { get; set; } = new List();
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs
new file mode 100644
index 0000000000..28f034d81b
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/AllHotkeyConflictsEventArgs.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class AllHotkeyConflictsEventArgs : EventArgs
+ {
+ public AllHotkeyConflictsData Conflicts { get; }
+
+ public AllHotkeyConflictsEventArgs(AllHotkeyConflictsData conflicts)
+ {
+ Conflicts = conflicts;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs
new file mode 100644
index 0000000000..a420ec7a2b
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class HotkeyConflictGroupData
+ {
+ public HotkeyData Hotkey { get; set; }
+
+ public bool IsSystemConflict { get; set; }
+
+ public List Modules { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs
new file mode 100644
index 0000000000..193eb39d89
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictInfo.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class HotkeyConflictInfo
+ {
+ public bool IsSystemConflict { get; set; }
+
+ public string ConflictingModuleName { get; set; }
+
+ public int ConflictingHotkeyID { get; set; }
+
+ public List AllConflictingModules { get; set; } = new List();
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs
new file mode 100644
index 0000000000..9e416db7d9
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyData.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.Settings.UI.Library.Utilities;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class HotkeyData
+ {
+ public bool Win { get; set; }
+
+ public bool Ctrl { get; set; }
+
+ public bool Shift { get; set; }
+
+ public bool Alt { get; set; }
+
+ public int Key { get; set; }
+
+ public List GetKeysList()
+ {
+ List shortcutList = new List();
+
+ 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;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleConflictsData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleConflictsData.cs
new file mode 100644
index 0000000000..2b343693bd
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleConflictsData.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class ModuleConflictsData
+ {
+ public List InAppConflicts { get; set; } = new List();
+
+ public List SystemConflicts { get; set; } = new List();
+
+ public bool HasConflicts => InAppConflicts.Count > 0 || SystemConflicts.Count > 0;
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs
new file mode 100644
index 0000000000..f24e02e650
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/ModuleHotkeyData.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+using Windows.Web.AtomPub;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
+{
+ public class ModuleHotkeyData : INotifyPropertyChanged
+ {
+ private string _moduleName;
+ private int _hotkeyID;
+ private HotkeySettings _hotkeySettings;
+ private bool _isSystemConflict;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public string IconPath { get; set; }
+
+ public string DisplayName { get; set; }
+
+ public string Header { get; set; }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public string ModuleName
+ {
+ get => _moduleName;
+ set
+ {
+ if (_moduleName != value)
+ {
+ _moduleName = value;
+ }
+ }
+ }
+
+ public int HotkeyID
+ {
+ get => _hotkeyID;
+ set
+ {
+ if (_hotkeyID != value)
+ {
+ _hotkeyID = value;
+ }
+ }
+ }
+
+ public HotkeySettings HotkeySettings
+ {
+ get => _hotkeySettings;
+ set
+ {
+ if (_hotkeySettings != value)
+ {
+ _hotkeySettings = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public bool IsSystemConflict
+ {
+ get => _isSystemConflict;
+ set
+ {
+ if (_isSystemConflict != value)
+ {
+ _isSystemConflict = value;
+ }
+ }
+ }
+
+ public ModuleType ModuleType { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs
index 89c1a1995d..724e1b5159 100644
--- a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs
+++ b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs
@@ -4,17 +4,29 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Globalization;
+using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public record HotkeySettings : ICmdLineRepresentable
+ public record HotkeySettings : ICmdLineRepresentable, INotifyPropertyChanged
{
private const int VKTAB = 0x09;
+ private bool _hasConflict;
+ private string _conflictDescription;
+ private bool _isSystemConflict;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
public HotkeySettings()
{
@@ -23,6 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = false;
Shift = false;
Code = 0;
+
+ HasConflict = false;
}
///
@@ -40,6 +54,51 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = alt;
Shift = shift;
Code = code;
+ HasConflict = false;
+ }
+
+ public bool HasConflict
+ {
+ get => _hasConflict;
+ set
+ {
+ if (_hasConflict != value)
+ {
+ _hasConflict = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public string ConflictDescription
+ {
+ get => _conflictDescription ?? string.Empty;
+ set
+ {
+ if (_conflictDescription != value)
+ {
+ _conflictDescription = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public bool IsSystemConflict
+ {
+ get => _isSystemConflict;
+ set
+ {
+ if (_isSystemConflict != value)
+ {
+ _isSystemConflict = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public virtual void UpdateConflictStatus()
+ {
+ Logger.LogInfo($"{this.ToString()}");
}
[JsonPropertyName("win")]
diff --git a/src/settings-ui/Settings.UI.Library/Interfaces/IHotkeyConfig.cs b/src/settings-ui/Settings.UI.Library/Interfaces/IHotkeyConfig.cs
new file mode 100644
index 0000000000..ee38f51cad
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Interfaces/IHotkeyConfig.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces
+{
+ public interface IHotkeyConfig
+ {
+ HotkeyAccessor[] GetAllHotkeyAccessors();
+
+ ModuleType GetModuleType();
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/MeasureToolSettings.cs b/src/settings-ui/Settings.UI.Library/MeasureToolSettings.cs
index 5720c70ca5..e2d034eb21 100644
--- a/src/settings-ui/Settings.UI.Library/MeasureToolSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/MeasureToolSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig
+ public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Measure Tool";
@@ -25,6 +27,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string GetModuleName()
=> Name;
+ public ModuleType GetModuleType() => ModuleType.MeasureTool;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "MeasureTool_ActivationShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
=> false;
diff --git a/src/settings-ui/Settings.UI.Library/MouseHighlighterSettings.cs b/src/settings-ui/Settings.UI.Library/MouseHighlighterSettings.cs
index e23a7fe288..54f28c026b 100644
--- a/src/settings-ui/Settings.UI.Library/MouseHighlighterSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/MouseHighlighterSettings.cs
@@ -2,15 +2,17 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig
+ public class MouseHighlighterSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "MouseHighlighter";
@@ -29,6 +31,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.MouseHighlighter;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "MouseUtils_MouseHighlighter_ActivationShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs
index 450e6aec93..a4c5a04555 100644
--- a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs
@@ -3,16 +3,18 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using MouseJump.Common.Helpers;
using MouseJump.Common.Models.Settings;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class MouseJumpSettings : BasePTModuleSettings, ISettingsConfig
+ public class MouseJumpSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "MouseJump";
@@ -46,6 +48,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.MouseJump;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "MouseUtils_MouseJump_ActivationShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs
index 9b0e530a2a..54542194c0 100644
--- a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs
@@ -13,9 +13,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, true, false, 0x50); // Win + Alt + P
+ [CmdConfigureIgnore]
+ public HotkeySettings DefaultGlidingCursorActivationShortcut => new HotkeySettings(true, false, true, false, 0xBE); // Win + Alt + .
+
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut { get; set; }
+ [JsonPropertyName("gliding_cursor_activation_shortcut")]
+ public HotkeySettings GlidingCursorActivationShortcut { get; set; }
+
[JsonPropertyName("crosshairs_color")]
public StringProperty CrosshairsColor { get; set; }
@@ -46,9 +52,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("auto_activate")]
public BoolProperty AutoActivate { get; set; }
+ [JsonPropertyName("gliding_travel_speed")]
+ public IntProperty GlidingTravelSpeed { get; set; }
+
+ [JsonPropertyName("gliding_delay_speed")]
+ public IntProperty GlidingDelaySpeed { get; set; }
+
public MousePointerCrosshairsProperties()
{
ActivationShortcut = DefaultActivationShortcut;
+ GlidingCursorActivationShortcut = DefaultGlidingCursorActivationShortcut;
CrosshairsColor = new StringProperty("#FF0000");
CrosshairsOpacity = new IntProperty(75);
CrosshairsRadius = new IntProperty(20);
@@ -59,6 +72,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
CrosshairsIsFixedLengthEnabled = new BoolProperty(false);
CrosshairsFixedLength = new IntProperty(1);
AutoActivate = new BoolProperty(false);
+ GlidingTravelSpeed = new IntProperty(25);
+ GlidingDelaySpeed = new IntProperty(5);
}
}
}
diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs
index 2658a2adec..d814f115a1 100644
--- a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class MousePointerCrosshairsSettings : BasePTModuleSettings, ISettingsConfig
+ public class MousePointerCrosshairsSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "MousePointerCrosshairs";
@@ -27,6 +29,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.MousePointerCrosshairs;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "MouseUtils_MousePointerCrosshairs_ActivationShortcut"),
+ new HotkeyAccessor(
+ () => Properties.GlidingCursorActivationShortcut,
+ value => Properties.GlidingCursorActivationShortcut = value ?? Properties.DefaultGlidingCursorActivationShortcut,
+ "MouseUtils_GlidingCursor"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
diff --git a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersSettings.cs b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersSettings.cs
index 6a51a150e5..3cab182fec 100644
--- a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersSettings.cs
@@ -3,15 +3,17 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class MouseWithoutBordersSettings : BasePTModuleSettings, ISettingsConfig
+ public class MouseWithoutBordersSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "MouseWithoutBorders";
@@ -37,6 +39,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.MouseWithoutBorders;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ToggleEasyMouseShortcut,
+ value => Properties.ToggleEasyMouseShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyToggleEasyMouse,
+ "MouseWithoutBorders_ToggleEasyMouseShortcut"),
+ new HotkeyAccessor(
+ () => Properties.LockMachineShortcut,
+ value => Properties.LockMachineShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyLockMachine,
+ "MouseWithoutBorders_LockMachinesShortcut"),
+ new HotkeyAccessor(
+ () => Properties.Switch2AllPCShortcut,
+ value => Properties.Switch2AllPCShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeySwitch2AllPC,
+ "MouseWithoutBorders_Switch2AllPcShortcut"),
+ new HotkeyAccessor(
+ () => Properties.ReconnectShortcut,
+ value => Properties.ReconnectShortcut = value ?? MouseWithoutBordersProperties.DefaultHotKeyReconnect,
+ "MouseWithoutBorders_ReconnectShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public HotkeySettings ConvertMouseWithoutBordersHotKeyToPowerToys(int value)
{
// VK_A <= value <= VK_Z
diff --git a/src/settings-ui/Settings.UI.Library/PeekSettings.cs b/src/settings-ui/Settings.UI.Library/PeekSettings.cs
index f5ad2a0e26..73993c72fa 100644
--- a/src/settings-ui/Settings.UI.Library/PeekSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/PeekSettings.cs
@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class PeekSettings : BasePTModuleSettings, ISettingsConfig
+ public class PeekSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Peek";
public const string ModuleVersion = "0.0.1";
@@ -35,6 +37,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.Peek;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "Activation_Shortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public bool UpgradeSettingsConfiguration()
{
return false;
diff --git a/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs b/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs
index c21ce67df5..18d2c2da1c 100644
--- a/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/PowerLauncherSettings.cs
@@ -6,12 +6,13 @@ using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class PowerLauncherSettings : BasePTModuleSettings, ISettingsConfig
+ public class PowerLauncherSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "PowerToys Run";
@@ -49,6 +50,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.PowerLauncher;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.OpenPowerLauncher,
+ value => Properties.OpenPowerLauncher = value ?? Properties.DefaultOpenPowerLauncher,
+ "PowerLauncher_OpenPowerLauncher"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
diff --git a/src/settings-ui/Settings.UI.Library/PowerOcrSettings.cs b/src/settings-ui/Settings.UI.Library/PowerOcrSettings.cs
index 46d176d2b0..4a296832ae 100644
--- a/src/settings-ui/Settings.UI.Library/PowerOcrSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/PowerOcrSettings.cs
@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class PowerOcrSettings : BasePTModuleSettings, ISettingsConfig
+ public class PowerOcrSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "TextExtractor";
@@ -42,6 +44,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string GetModuleName()
=> Name;
+ public ModuleType GetModuleType() => ModuleType.PowerOCR;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.ActivationShortcut,
+ value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut,
+ "Activation_Shortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
=> false;
diff --git a/src/settings-ui/Settings.UI.Library/SettingsFactory.cs b/src/settings-ui/Settings.UI.Library/SettingsFactory.cs
new file mode 100644
index 0000000000..2bb9e79121
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/SettingsFactory.cs
@@ -0,0 +1,197 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+
+namespace Microsoft.PowerToys.Settings.UI.Services
+{
+ ///
+ /// Factory service for getting PowerToys module Settings that implement IHotkeyConfig
+ ///
+ public class SettingsFactory
+ {
+ private readonly ISettingsUtils _settingsUtils;
+ private readonly Dictionary _settingsTypes;
+
+ public SettingsFactory(ISettingsUtils settingsUtils)
+ {
+ _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
+ _settingsTypes = DiscoverSettingsTypes();
+ }
+
+ ///
+ /// Dynamically discovers all Settings types that implement IHotkeyConfig
+ ///
+ private Dictionary DiscoverSettingsTypes()
+ {
+ var settingsTypes = new Dictionary();
+
+ // Get the Settings.UI.Library assembly
+ var assembly = Assembly.GetAssembly(typeof(IHotkeyConfig));
+ if (assembly == null)
+ {
+ return settingsTypes;
+ }
+
+ try
+ {
+ // Find all types that implement IHotkeyConfig and ISettingsConfig
+ var hotkeyConfigTypes = assembly.GetTypes()
+ .Where(type =>
+ type.IsClass &&
+ !type.IsAbstract &&
+ typeof(IHotkeyConfig).IsAssignableFrom(type) &&
+ typeof(ISettingsConfig).IsAssignableFrom(type))
+ .ToList();
+
+ foreach (var type in hotkeyConfigTypes)
+ {
+ // Try to get the ModuleName using SettingsRepository
+ try
+ {
+ var repositoryType = typeof(SettingsRepository<>).MakeGenericType(type);
+ var getInstanceMethod = repositoryType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static);
+ var repository = getInstanceMethod?.Invoke(null, new object[] { _settingsUtils });
+
+ if (repository != null)
+ {
+ var settingsConfigProperty = repository.GetType().GetProperty("SettingsConfig");
+ var settingsInstance = settingsConfigProperty?.GetValue(repository) as ISettingsConfig;
+
+ if (settingsInstance != null)
+ {
+ var moduleName = settingsInstance.GetModuleName();
+ if (!string.IsNullOrEmpty(moduleName))
+ {
+ settingsTypes[moduleName] = type;
+ System.Diagnostics.Debug.WriteLine($"Discovered settings type: {type.Name} for module: {moduleName}");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error getting module name for {type.Name}: {ex.Message}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error scanning assembly {assembly.FullName}: {ex.Message}");
+ }
+
+ return settingsTypes;
+ }
+
+ public IHotkeyConfig GetFreshSettings(string moduleKey)
+ {
+ if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType))
+ {
+ return null;
+ }
+
+ try
+ {
+ // Create a generic method call to _settingsUtils.GetSettingsOrDefault(moduleKey)
+ var getSettingsMethod = typeof(ISettingsUtils).GetMethod("GetSettingsOrDefault", new[] { typeof(string), typeof(string) });
+ var genericMethod = getSettingsMethod?.MakeGenericMethod(settingsType);
+
+ // Call GetSettingsOrDefault(moduleKey) to get fresh settings from file
+ var freshSettings = genericMethod?.Invoke(_settingsUtils, new object[] { moduleKey, "settings.json" });
+
+ return freshSettings as IHotkeyConfig;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error getting fresh settings for {moduleKey}: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Gets a settings instance for the specified module using SettingsRepository
+ ///
+ /// The module key/name
+ /// The settings instance implementing IHotkeyConfig, or null if not found
+ public IHotkeyConfig GetSettings(string moduleKey)
+ {
+ if (!_settingsTypes.TryGetValue(moduleKey, out var settingsType))
+ {
+ return null;
+ }
+
+ try
+ {
+ var repositoryType = typeof(SettingsRepository<>).MakeGenericType(settingsType);
+ var getInstanceMethod = repositoryType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static);
+ var repository = getInstanceMethod?.Invoke(null, new object[] { _settingsUtils });
+
+ if (repository != null)
+ {
+ var settingsConfigProperty = repository.GetType().GetProperty("SettingsConfig");
+ return settingsConfigProperty?.GetValue(repository) as IHotkeyConfig;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error getting Settings for {moduleKey}: {ex.Message}");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets all available module names that have settings implementing IHotkeyConfig
+ ///
+ /// List of module names
+ public List GetAvailableModuleNames()
+ {
+ return _settingsTypes.Keys.ToList();
+ }
+
+ ///
+ /// Gets all available settings that implement IHotkeyConfig
+ ///
+ /// Dictionary of module name to settings instance
+ public Dictionary GetAllHotkeySettings()
+ {
+ var result = new Dictionary();
+
+ foreach (var moduleKey in _settingsTypes.Keys)
+ {
+ try
+ {
+ var settings = GetSettings(moduleKey);
+ if (settings != null)
+ {
+ result[moduleKey] = settings;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error getting settings for {moduleKey}: {ex.Message}");
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Gets a specific settings repository instance
+ ///
+ /// The settings type
+ /// The settings repository instance
+ public ISettingsRepository GetRepository()
+ where T : class, ISettingsConfig, new()
+ {
+ return SettingsRepository.GetInstance(_settingsUtils);
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/ShortcutGuideSettings.cs b/src/settings-ui/Settings.UI.Library/ShortcutGuideSettings.cs
index c39e757fe3..40174aeb81 100644
--- a/src/settings-ui/Settings.UI.Library/ShortcutGuideSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/ShortcutGuideSettings.cs
@@ -2,13 +2,15 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class ShortcutGuideSettings : BasePTModuleSettings, ISettingsConfig
+ public class ShortcutGuideSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Shortcut Guide";
@@ -27,6 +29,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return Name;
}
+ public ModuleType GetModuleType() => ModuleType.ShortcutGuide;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.OpenShortcutGuide,
+ value => Properties.OpenShortcutGuide = value ?? Properties.DefaultOpenShortcutGuide,
+ "Activation_Shortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs
new file mode 100644
index 0000000000..3f5b8e9964
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs
@@ -0,0 +1,20 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
+{
+ [EventData]
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
+ public class ShortcutConflictControlClickedEvent : EventBase, IEvent
+ {
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+
+ public int ConflictCount { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs
new file mode 100644
index 0000000000..b8d7c13497
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs
@@ -0,0 +1,20 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
+{
+ [EventData]
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
+ public class ShortcutConflictDetectedEvent : EventBase, IEvent
+ {
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+
+ public int ConflictCount { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs
new file mode 100644
index 0000000000..7f5bf56e82
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs
@@ -0,0 +1,20 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
+{
+ [EventData]
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
+ public class ShortcutConflictResolvedEvent : EventBase, IEvent
+ {
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+
+ public string Source { get; set; }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.Library/WorkspacesSettings.cs b/src/settings-ui/Settings.UI.Library/WorkspacesSettings.cs
index 1e3ce2261e..fafb034935 100644
--- a/src/settings-ui/Settings.UI.Library/WorkspacesSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/WorkspacesSettings.cs
@@ -3,14 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
-
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
- public class WorkspacesSettings : BasePTModuleSettings, ISettingsConfig
+ public class WorkspacesSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Workspaces";
public const string ModuleVersion = "0.0.1";
@@ -39,6 +41,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return false;
}
+ public ModuleType GetModuleType() => ModuleType.Workspaces;
+
+ public HotkeyAccessor[] GetAllHotkeyAccessors()
+ {
+ var hotkeyAccessors = new List
+ {
+ new HotkeyAccessor(
+ () => Properties.Hotkey.Value,
+ value => Properties.Hotkey.Value = value ?? WorkspacesProperties.DefaultHotkeyValue,
+ "Workspaces_ActivationShortcut"),
+ };
+
+ return hotkeyAccessors.ToArray();
+ }
+
public virtual void Save(ISettingsUtils settingsUtils)
{
// Save settings to file
diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/PowerLauncherViewModelTest.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/PowerLauncherViewModelTest.cs
index f1084d498a..59b61559f4 100644
--- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/PowerLauncherViewModelTest.cs
+++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/PowerLauncherViewModelTest.cs
@@ -3,8 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
-
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -13,7 +13,7 @@ using Moq;
namespace ViewModelTests
{
[TestClass]
- public class PowerLauncherViewModelTest
+ public class PowerLauncherViewModelTest : IDisposable
{
private sealed class SendCallbackMock
{
@@ -26,20 +26,48 @@ namespace ViewModelTests
{
TimesSent++;
}
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "We actually don't validate setting, just calculate it was sent")]
+ public int OnSendIPC(string _)
+ {
+ TimesSent++;
+ return 0;
+ }
}
private PowerLauncherViewModel viewModel;
private PowerLauncherSettings mockSettings;
private SendCallbackMock sendCallbackMock;
+ private BackCompatTestProperties.MockSettingsRepository mockGeneralSettingsRepository;
[TestInitialize]
public void Initialize()
{
mockSettings = new PowerLauncherSettings();
sendCallbackMock = new SendCallbackMock();
+
+ var settingPathMock = new Mock();
+ var mockGeneralIOProvider = BackCompatTestProperties.GetGeneralSettingsIOProvider("v0.22.0");
+ var mockGeneralSettingsUtils = new SettingsUtils(mockGeneralIOProvider.Object, settingPathMock.Object);
+ mockGeneralSettingsRepository = new BackCompatTestProperties.MockSettingsRepository(mockGeneralSettingsUtils);
+
viewModel = new PowerLauncherViewModel(
mockSettings,
- new PowerLauncherViewModel.SendCallback(sendCallbackMock.OnSend));
+ mockGeneralSettingsRepository,
+ sendCallbackMock.OnSendIPC,
+ () => false);
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ viewModel?.Dispose();
+ }
+
+ public void Dispose()
+ {
+ viewModel?.Dispose();
+ GC.SuppressFinalize(this);
}
///
@@ -67,7 +95,7 @@ namespace ViewModelTests
// Initialise View Model with test Config files
Func sendMockIPCConfigMSG = msg => { return 0; };
- PowerLauncherViewModel viewModel = new PowerLauncherViewModel(originalSettings, generalSettingsRepository, sendMockIPCConfigMSG, () => true);
+ using PowerLauncherViewModel viewModel = new PowerLauncherViewModel(originalSettings, generalSettingsRepository, sendMockIPCConfigMSG, () => true);
// Verify that the old settings persisted
Assert.AreEqual(originalGeneralSettings.Enabled.PowerLauncher, viewModel.EnablePowerLauncher);
diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseCrosshairs.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseCrosshairs.png
index ae940629b0..dea5a249b5 100644
Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseCrosshairs.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseCrosshairs.png differ
diff --git a/src/settings-ui/Settings.UI/Converters/BoolToConflictTypeConverter.cs b/src/settings-ui/Settings.UI/Converters/BoolToConflictTypeConverter.cs
new file mode 100644
index 0000000000..826bfa19dd
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/BoolToConflictTypeConverter.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Microsoft.UI.Xaml.Data;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters
+{
+ public partial class BoolToConflictTypeConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ if (value is bool isSystemConflict)
+ {
+ return isSystemConflict ? "System Conflict" : "In-App Conflict";
+ }
+
+ return "Unknown Conflict";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Helpers/HotkeyConflictHelper.cs b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictHelper.cs
new file mode 100644
index 0000000000..d7f56fceea
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictHelper.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Nodes;
+using Microsoft.PowerToys.Settings.UI.Library;
+
+namespace Microsoft.PowerToys.Settings.UI.Helpers
+{
+ public class HotkeyConflictHelper
+ {
+ public delegate void HotkeyConflictCheckCallback(bool hasConflict, HotkeyConflictResponse conflicts);
+
+ private static readonly Dictionary PendingHotkeyConflictChecks = new Dictionary();
+ private static readonly object LockObject = new object();
+
+ public static void CheckHotkeyConflict(HotkeySettings hotkeySettings, Func ipcMSGCallBackFunc, HotkeyConflictCheckCallback callback)
+ {
+ if (hotkeySettings == null || ipcMSGCallBackFunc == null)
+ {
+ return;
+ }
+
+ string requestId = GenerateRequestId();
+
+ lock (LockObject)
+ {
+ PendingHotkeyConflictChecks[requestId] = callback;
+ }
+
+ var hotkeyObj = new JsonObject
+ {
+ ["request_id"] = requestId,
+ ["win"] = hotkeySettings.Win,
+ ["ctrl"] = hotkeySettings.Ctrl,
+ ["shift"] = hotkeySettings.Shift,
+ ["alt"] = hotkeySettings.Alt,
+ ["key"] = hotkeySettings.Code,
+ };
+
+ var requestObject = new JsonObject
+ {
+ ["check_hotkey_conflict"] = hotkeyObj,
+ };
+
+ ipcMSGCallBackFunc(requestObject.ToString());
+ }
+
+ public static void HandleHotkeyConflictResponse(HotkeyConflictResponse response)
+ {
+ if (response.AllConflicts.Count == 0)
+ {
+ return;
+ }
+
+ HotkeyConflictCheckCallback callback = null;
+
+ lock (LockObject)
+ {
+ if (PendingHotkeyConflictChecks.TryGetValue(response.RequestId, out callback))
+ {
+ PendingHotkeyConflictChecks.Remove(response.RequestId);
+ }
+ }
+
+ callback?.Invoke(response.HasConflict, response);
+ }
+
+ private static string GenerateRequestId() => Guid.NewGuid().ToString();
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Helpers/HotkeyConflictResponse.cs b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictResponse.cs
new file mode 100644
index 0000000000..90803df64c
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictResponse.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+
+namespace Microsoft.PowerToys.Settings.UI.Helpers
+{
+ public class HotkeyConflictResponse
+ {
+ public string RequestId { get; set; }
+
+ public bool HasConflict { get; set; }
+
+ public List AllConflicts { get; set; } = new List();
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
index 8fd948fd86..bd72be5f8c 100644
--- a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
+++ b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
@@ -13,23 +13,29 @@ using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
-[JsonSerializable(typeof(WINDOWPLACEMENT))]
+[JsonSerializable(typeof(ActionMessage))]
[JsonSerializable(typeof(AdvancedPasteSettings))]
-[JsonSerializable(typeof(Dictionary>))]
[JsonSerializable(typeof(AlwaysOnTopSettings))]
[JsonSerializable(typeof(ColorPickerSettings))]
[JsonSerializable(typeof(CropAndLockSettings))]
+[JsonSerializable(typeof(Dictionary>))]
[JsonSerializable(typeof(FileLocksmithSettings))]
+[JsonSerializable(typeof(FindMyMouseSettings))]
+[JsonSerializable(typeof(IList))]
[JsonSerializable(typeof(MeasureToolSettings))]
+[JsonSerializable(typeof(MouseHighlighterSettings))]
+[JsonSerializable(typeof(MouseJumpSettings))]
+[JsonSerializable(typeof(MousePointerCrosshairsSettings))]
[JsonSerializable(typeof(MouseWithoutBordersSettings))]
[JsonSerializable(typeof(NewPlusSettings))]
[JsonSerializable(typeof(PeekSettings))]
[JsonSerializable(typeof(PowerLauncherSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
+[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(RegistryPreviewSettings))]
+[JsonSerializable(typeof(ShortcutGuideSettings))]
+[JsonSerializable(typeof(WINDOWPLACEMENT))]
[JsonSerializable(typeof(WorkspacesSettings))]
-[JsonSerializable(typeof(IList))]
-[JsonSerializable(typeof(ActionMessage))]
public sealed partial class SourceGenerationContextContext : JsonSerializerContext
{
}
diff --git a/src/settings-ui/Settings.UI/Services/GlobalHotkeyConflictManager.cs b/src/settings-ui/Settings.UI/Services/GlobalHotkeyConflictManager.cs
new file mode 100644
index 0000000000..3971c0589e
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Services/GlobalHotkeyConflictManager.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+
+namespace Microsoft.PowerToys.Settings.UI.Services
+{
+ public class GlobalHotkeyConflictManager
+ {
+ private readonly Func _sendIPCMessage;
+
+ private static GlobalHotkeyConflictManager _instance;
+ private AllHotkeyConflictsData _currentConflicts = new AllHotkeyConflictsData();
+
+ public static GlobalHotkeyConflictManager Instance => _instance;
+
+ public static void Initialize(Func sendIPCMessage)
+ {
+ _instance = new GlobalHotkeyConflictManager(sendIPCMessage);
+ }
+
+ private GlobalHotkeyConflictManager(Func sendIPCMessage)
+ {
+ _sendIPCMessage = sendIPCMessage;
+
+ IPCResponseService.AllHotkeyConflictsReceived += OnAllHotkeyConflictsReceived;
+ }
+
+ public event EventHandler ConflictsUpdated;
+
+ public void RequestAllConflicts()
+ {
+ var requestMessage = "{\"get_all_hotkey_conflicts\":{}}";
+ _sendIPCMessage?.Invoke(requestMessage);
+ }
+
+ private void OnAllHotkeyConflictsReceived(object sender, AllHotkeyConflictsEventArgs e)
+ {
+ _currentConflicts = e.Conflicts;
+ ConflictsUpdated?.Invoke(this, e);
+ }
+
+ public bool HasConflictForHotkey(HotkeySettings hotkey, string moduleName, int hotkeyID)
+ {
+ if (hotkey == null)
+ {
+ return false;
+ }
+
+ var allConflictGroups = _currentConflicts.InAppConflicts.Concat(_currentConflicts.SystemConflicts);
+
+ foreach (var group in allConflictGroups)
+ {
+ if (IsHotkeyMatch(hotkey, group.Hotkey))
+ {
+ if (!string.IsNullOrEmpty(moduleName) && hotkeyID >= 0)
+ {
+ var selfModule = group.Modules.FirstOrDefault(m =>
+ m.ModuleName.Equals(moduleName, StringComparison.OrdinalIgnoreCase) &&
+ m.HotkeyID == hotkeyID);
+
+ if (selfModule != null && group.Modules.Count == 1)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public HotkeyConflictInfo GetConflictInfo(HotkeySettings hotkey)
+ {
+ if (hotkey == null)
+ {
+ return null;
+ }
+
+ var allConflictGroups = _currentConflicts.InAppConflicts.Concat(_currentConflicts.SystemConflicts);
+
+ foreach (var group in allConflictGroups)
+ {
+ if (IsHotkeyMatch(hotkey, group.Hotkey))
+ {
+ var conflictModules = group.Modules.Where(m => m != null).ToList();
+ if (conflictModules.Count != 0)
+ {
+ var firstModule = conflictModules.First();
+ return new HotkeyConflictInfo
+ {
+ IsSystemConflict = group.IsSystemConflict,
+ ConflictingModuleName = firstModule.ModuleName,
+ ConflictingHotkeyID = firstModule.HotkeyID,
+ AllConflictingModules = conflictModules.Select(m => $"{m.ModuleName}:{m.HotkeyID}").ToList(),
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private bool IsHotkeyMatch(HotkeySettings settings, HotkeyData data)
+ {
+ return settings.Win == data.Win &&
+ settings.Ctrl == data.Ctrl &&
+ settings.Shift == data.Shift &&
+ settings.Alt == data.Alt &&
+ settings.Code == data.Key;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Services/IPCResponseService.cs b/src/settings-ui/Settings.UI/Services/IPCResponseService.cs
new file mode 100644
index 0000000000..ed16b43603
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Services/IPCResponseService.cs
@@ -0,0 +1,199 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Windows.Data.Json;
+
+namespace Microsoft.PowerToys.Settings.UI.Services
+{
+ public class IPCResponseService
+ {
+ private static IPCResponseService _instance;
+
+ public static IPCResponseService Instance => _instance ??= new IPCResponseService();
+
+ public static event EventHandler AllHotkeyConflictsReceived;
+
+ public void RegisterForIPC()
+ {
+ ShellPage.ShellHandler?.IPCResponseHandleList.Add(ProcessIPCMessage);
+ }
+
+ public void UnregisterFromIPC()
+ {
+ ShellPage.ShellHandler?.IPCResponseHandleList.Remove(ProcessIPCMessage);
+ }
+
+ private void ProcessIPCMessage(JsonObject json)
+ {
+ try
+ {
+ if (json.TryGetValue("response_type", out IJsonValue responseTypeValue) &&
+ responseTypeValue.ValueType == JsonValueType.String)
+ {
+ string responseType = responseTypeValue.GetString();
+
+ if (responseType.Equals("hotkey_conflict_result", StringComparison.Ordinal))
+ {
+ ProcessHotkeyConflictResult(json);
+ }
+ else if (responseType.Equals("all_hotkey_conflicts", StringComparison.Ordinal))
+ {
+ ProcessAllHotkeyConflicts(json);
+ }
+ }
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ private void ProcessHotkeyConflictResult(JsonObject json)
+ {
+ string requestId = string.Empty;
+ if (json.TryGetValue("request_id", out IJsonValue requestIdValue) &&
+ requestIdValue.ValueType == JsonValueType.String)
+ {
+ requestId = requestIdValue.GetString();
+ }
+
+ bool hasConflict = false;
+ if (json.TryGetValue("has_conflict", out IJsonValue hasConflictValue) &&
+ hasConflictValue.ValueType == JsonValueType.Boolean)
+ {
+ hasConflict = hasConflictValue.GetBoolean();
+ }
+
+ var allConflicts = new List();
+
+ if (hasConflict)
+ {
+ // Parse the all_conflicts array
+ if (json.TryGetValue("all_conflicts", out IJsonValue allConflictsValue) &&
+ allConflictsValue.ValueType == JsonValueType.Array)
+ {
+ var conflictsArray = allConflictsValue.GetArray();
+ foreach (var conflictItem in conflictsArray)
+ {
+ if (conflictItem.ValueType == JsonValueType.Object)
+ {
+ var conflictObj = conflictItem.GetObject();
+
+ string moduleName = string.Empty;
+ int hotkeyID = -1;
+
+ if (conflictObj.TryGetValue("module", out IJsonValue moduleValue) &&
+ moduleValue.ValueType == JsonValueType.String)
+ {
+ moduleName = moduleValue.GetString();
+ }
+
+ if (conflictObj.TryGetValue("hotkeyID", out IJsonValue hotkeyValue) &&
+ hotkeyValue.ValueType == JsonValueType.Number)
+ {
+ hotkeyID = (int)hotkeyValue.GetNumber();
+ }
+
+ allConflicts.Add(new ModuleHotkeyData
+ {
+ ModuleName = moduleName,
+ HotkeyID = hotkeyID,
+ });
+ }
+ }
+ }
+ }
+
+ var response = new HotkeyConflictResponse
+ {
+ RequestId = requestId,
+ HasConflict = hasConflict,
+ AllConflicts = allConflicts,
+ };
+
+ HotkeyConflictHelper.HandleHotkeyConflictResponse(response);
+ }
+
+ private void ProcessAllHotkeyConflicts(JsonObject json)
+ {
+ var allConflicts = new AllHotkeyConflictsData();
+
+ if (json.TryGetValue("inAppConflicts", out IJsonValue inAppValue) &&
+ inAppValue.ValueType == JsonValueType.Array)
+ {
+ var inAppArray = inAppValue.GetArray();
+ foreach (var conflictGroup in inAppArray)
+ {
+ var conflictObj = conflictGroup.GetObject();
+ var conflictData = ParseConflictGroup(conflictObj, false);
+ if (conflictData != null)
+ {
+ allConflicts.InAppConflicts.Add(conflictData);
+ }
+ }
+ }
+
+ if (json.TryGetValue("sysConflicts", out IJsonValue sysValue) &&
+ sysValue.ValueType == JsonValueType.Array)
+ {
+ var sysArray = sysValue.GetArray();
+ foreach (var conflictGroup in sysArray)
+ {
+ var conflictObj = conflictGroup.GetObject();
+ var conflictData = ParseConflictGroup(conflictObj, true);
+ if (conflictData != null)
+ {
+ allConflicts.SystemConflicts.Add(conflictData);
+ }
+ }
+ }
+
+ AllHotkeyConflictsReceived?.Invoke(this, new AllHotkeyConflictsEventArgs(allConflicts));
+ }
+
+ private HotkeyConflictGroupData ParseConflictGroup(JsonObject conflictObj, bool isSystemConflict)
+ {
+ if (!conflictObj.TryGetValue("hotkey", out var hotkeyValue) ||
+ !conflictObj.TryGetValue("modules", out var modulesValue))
+ {
+ return null;
+ }
+
+ var hotkeyObj = hotkeyValue.GetObject();
+ bool win = hotkeyObj.TryGetValue("win", out var winVal) && winVal.GetBoolean();
+ bool ctrl = hotkeyObj.TryGetValue("ctrl", out var ctrlVal) && ctrlVal.GetBoolean();
+ bool shift = hotkeyObj.TryGetValue("shift", out var shiftVal) && shiftVal.GetBoolean();
+ bool alt = hotkeyObj.TryGetValue("alt", out var altVal) && altVal.GetBoolean();
+ int key = hotkeyObj.TryGetValue("key", out var keyVal) ? (int)keyVal.GetNumber() : 0;
+
+ var conflictGroup = new HotkeyConflictGroupData
+ {
+ Hotkey = new HotkeyData { Win = win, Ctrl = ctrl, Shift = shift, Alt = alt, Key = key },
+ IsSystemConflict = isSystemConflict,
+ Modules = new List(),
+ };
+
+ var modulesArray = modulesValue.GetArray();
+ foreach (var module in modulesArray)
+ {
+ var moduleObj = module.GetObject();
+ string moduleName = moduleObj.TryGetValue("moduleName", out var modNameVal) ? modNameVal.GetString() : string.Empty;
+ int hotkeyID = moduleObj.TryGetValue("hotkeyID", out var hotkeyIDVal) ? (int)hotkeyIDVal.GetNumber() : -1;
+
+ conflictGroup.Modules.Add(new ModuleHotkeyData
+ {
+ ModuleName = moduleName,
+ HotkeyID = hotkeyID,
+ });
+ }
+
+ return conflictGroup;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
index f3649555fd..d5bd0977e1 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs
@@ -232,6 +232,12 @@ namespace Microsoft.PowerToys.Settings.UI
});
ipcmanager.Start();
+ GlobalHotkeyConflictManager.Initialize(message =>
+ {
+ ipcmanager.Send(message);
+ return 0;
+ });
+
if (!ShowOobe && !ShowScoobe && !ShowFlyout)
{
settingsWindow = new MainWindow();
@@ -320,10 +326,18 @@ namespace Microsoft.PowerToys.Settings.UI
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
settingsWindow.Activate();
settingsWindow.NavigateToSection(StartupPage);
+
+ // In DEBUG mode, we might not have IPC set up, so provide a dummy implementation
+ GlobalHotkeyConflictManager.Initialize(message =>
+ {
+ // In debug mode, just log or do nothing
+ System.Diagnostics.Debug.WriteLine($"IPC Message: {message}");
+ return 0;
+ });
#else
- /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */
- Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true);
- Exit();
+ /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */
+ Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true);
+ Exit();
#endif
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml
index ed3e153682..69a7a1084d 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml
@@ -8,7 +8,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
-
+
-
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs
index 9b0c0f4574..7195b159e1 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs
@@ -4,37 +4,143 @@
using System;
using System.Collections.Generic;
-using System.IO;
+using System.ComponentModel;
using System.Linq;
-using System.Runtime.InteropServices.WindowsRuntime;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
+using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
+using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Controls.Primitives;
-using Microsoft.UI.Xaml.Data;
-using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Media;
-using Microsoft.UI.Xaml.Navigation;
-using Windows.Foundation;
-using Windows.Foundation.Collections;
+using Microsoft.Windows.ApplicationModel.Resources;
namespace Microsoft.PowerToys.Settings.UI.Controls
{
- public sealed partial class ShortcutConflictControl : UserControl
+ public sealed partial class ShortcutConflictControl : UserControl, INotifyPropertyChanged
{
+ private static readonly ResourceLoader ResourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+
+ private static bool _telemetryEventSent;
+
+ public static readonly DependencyProperty AllHotkeyConflictsDataProperty =
+ DependencyProperty.Register(
+ nameof(AllHotkeyConflictsData),
+ typeof(AllHotkeyConflictsData),
+ typeof(ShortcutConflictControl),
+ new PropertyMetadata(null, OnAllHotkeyConflictsDataChanged));
+
+ public AllHotkeyConflictsData AllHotkeyConflictsData
+ {
+ get => (AllHotkeyConflictsData)GetValue(AllHotkeyConflictsDataProperty);
+ set => SetValue(AllHotkeyConflictsDataProperty, value);
+ }
+
+ public int ConflictCount
+ {
+ get
+ {
+ if (AllHotkeyConflictsData == null)
+ {
+ return 0;
+ }
+
+ int count = 0;
+ if (AllHotkeyConflictsData.InAppConflicts != null)
+ {
+ count += AllHotkeyConflictsData.InAppConflicts.Count;
+ }
+
+ if (AllHotkeyConflictsData.SystemConflicts != null)
+ {
+ count += AllHotkeyConflictsData.SystemConflicts.Count;
+ }
+
+ return count;
+ }
+ }
+
+ public string ConflictText
+ {
+ get
+ {
+ var count = ConflictCount;
+ return count switch
+ {
+ 0 => ResourceLoader.GetString("ShortcutConflictControl_NoConflictsFound"),
+ 1 => ResourceLoader.GetString("ShortcutConflictControl_SingleConflictFound"),
+ _ => string.Format(
+ System.Globalization.CultureInfo.CurrentCulture,
+ ResourceLoader.GetString("ShortcutConflictControl_MultipleConflictsFound"),
+ count),
+ };
+ }
+ }
+
+ public bool HasConflicts => ConflictCount > 0;
+
+ private static void OnAllHotkeyConflictsDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is ShortcutConflictControl control)
+ {
+ control.UpdateProperties();
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ private void UpdateProperties()
+ {
+ OnPropertyChanged(nameof(ConflictCount));
+ OnPropertyChanged(nameof(ConflictText));
+ OnPropertyChanged(nameof(HasConflicts));
+
+ // Update visibility based on conflict count
+ Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed;
+
+ if (!_telemetryEventSent && HasConflicts)
+ {
+ // Log telemetry event when conflicts are detected
+ PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictDetectedEvent()
+ {
+ ConflictCount = ConflictCount,
+ });
+
+ _telemetryEventSent = true;
+ }
+ }
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
public ShortcutConflictControl()
{
InitializeComponent();
- GetShortcutConflicts();
- }
+ DataContext = this;
- private void GetShortcutConflicts()
- {
- // TO DO: Implement the logic to retrieve and display shortcut conflicts. Make sure to Collapse this control if not conflicts are found.
+ // Initially hide the control if no conflicts
+ Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed;
}
private void ShortcutConflictBtn_Click(object sender, RoutedEventArgs e)
{
- // TO DO: Handle the button click event to show the shortcut conflicts window.
+ if (AllHotkeyConflictsData == null || !HasConflicts)
+ {
+ return;
+ }
+
+ // Log telemetry event when user clicks the shortcut conflict button
+ PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictControlClickedEvent()
+ {
+ ConflictCount = this.ConflictCount,
+ });
+
+ // Create and show the new window instead of dialog
+ var conflictWindow = new ShortcutConflictWindow();
+
+ // Show the window
+ conflictWindow.Activate();
}
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml
new file mode 100644
index 0000000000..46f8d4f962
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs
new file mode 100644
index 0000000000..5bcc282261
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using CommunityToolkit.WinUI.Controls;
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+using Microsoft.PowerToys.Settings.UI.Services;
+using Microsoft.PowerToys.Settings.UI.ViewModels;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Microsoft.UI;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using Windows.Graphics;
+using WinUIEx;
+
+namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
+{
+ public sealed partial class ShortcutConflictWindow : WindowEx
+ {
+ public ShortcutConflictViewModel DataContext { get; }
+
+ public ShortcutConflictViewModel ViewModel { get; private set; }
+
+ public ShortcutConflictWindow()
+ {
+ var settingsUtils = new SettingsUtils();
+ ViewModel = new ShortcutConflictViewModel(
+ settingsUtils,
+ SettingsRepository.GetInstance(settingsUtils),
+ ShellPage.SendDefaultIPCMessage);
+
+ DataContext = ViewModel;
+ InitializeComponent();
+
+ this.Activated += Window_Activated_SetIcon;
+
+ // Set localized window title
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+ this.ExtendsContentIntoTitleBar = true;
+
+ this.Title = resourceLoader.GetString("ShortcutConflictWindow_Title");
+ this.CenterOnScreen();
+
+ ViewModel.OnPageLoaded();
+ }
+
+ private void CenterOnScreen()
+ {
+ var displayArea = DisplayArea.GetFromWindowId(this.AppWindow.Id, DisplayAreaFallback.Nearest);
+ if (displayArea != null)
+ {
+ var windowSize = this.AppWindow.Size;
+ var centeredPosition = new PointInt32
+ {
+ X = (displayArea.WorkArea.Width - windowSize.Width) / 2,
+ Y = (displayArea.WorkArea.Height - windowSize.Height) / 2,
+ };
+ this.AppWindow.Move(centeredPosition);
+ }
+ }
+
+ private void SettingsCard_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is SettingsCard settingsCard &&
+ settingsCard.DataContext is ModuleHotkeyData moduleData)
+ {
+ var moduleType = moduleData.ModuleType;
+ NavigationService.Navigate(ModuleHelper.GetModulePageType(moduleType));
+ this.Close();
+ }
+ }
+
+ private void WindowEx_Closed(object sender, WindowEventArgs args)
+ {
+ ViewModel?.Dispose();
+ }
+
+ private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args)
+ {
+ // Set window icon
+ var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
+ WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
+ AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
+ appWindow.SetIcon("Assets\\Settings\\icon.ico");
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
index 9ec7f4a2ec..931286ceaf 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
@@ -188,4 +188,4 @@
-
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml
index 72cb4a3c55..d81be4aa6c 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml
@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
+ xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="LayoutRoot"
@@ -39,6 +40,7 @@
Content="{Binding}"
CornerRadius="{StaticResource ControlCornerRadius}"
FontWeight="SemiBold"
+ IsInvalid="{Binding ElementName=LayoutRoot, Path=HasConflict}"
IsTabStop="False"
Style="{StaticResource AccentKeyVisualStyle}" />
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs
index c75017300c..5b21743d5f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs
@@ -3,18 +3,33 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
using CommunityToolkit.WinUI;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
+using Microsoft.PowerToys.Settings.UI.Services;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.ApplicationModel.Resources;
using Windows.System;
namespace Microsoft.PowerToys.Settings.UI.Controls
{
+ public enum ShortcutControlSource
+ {
+ SettingsPage,
+ ConflictWindow,
+ }
+
public sealed partial class ShortcutControl : UserControl, IDisposable
{
private readonly UIntPtr ignoreKeyEventFlag = (UIntPtr)0x5555;
@@ -33,8 +48,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("Enabled", typeof(bool), typeof(ShortcutControl), null);
public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register("HotkeySettings", typeof(HotkeySettings), typeof(ShortcutControl), null);
-
public static readonly DependencyProperty AllowDisableProperty = DependencyProperty.Register("AllowDisable", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnAllowDisableChanged));
+ public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnHasConflictChanged));
+ public static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip", typeof(string), typeof(ShortcutControl), new PropertyMetadata(null, OnTooltipChanged));
+
+ // Dependency property to track the source/context of the ShortcutControl
+ public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ShortcutControlSource), typeof(ShortcutControl), new PropertyMetadata(ShortcutControlSource.SettingsPage));
private static ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
@@ -58,6 +77,69 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
description.Text = text;
}
+ private static void OnHasConflictChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = d as ShortcutControl;
+ if (control == null)
+ {
+ return;
+ }
+
+ control.UpdateKeyVisualStyles();
+
+ // Check if conflict was resolved (had conflict before, no conflict now)
+ var oldValue = (bool)(e.OldValue ?? false);
+ var newValue = (bool)(e.NewValue ?? false);
+
+ // General conflict resolution telemetry (for all sources)
+ if (oldValue && !newValue)
+ {
+ // Determine the actual source based on the control's context
+ var actualSource = DetermineControlSource(control);
+
+ // Conflict was resolved - send general telemetry
+ PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictResolvedEvent()
+ {
+ Source = actualSource.ToString(),
+ });
+ }
+ }
+
+ private static ShortcutControlSource DetermineControlSource(ShortcutControl control)
+ {
+ // Walk up the visual tree to find the parent window/container
+ DependencyObject parent = control;
+ while (parent != null)
+ {
+ parent = VisualTreeHelper.GetParent(parent);
+
+ // Check if we're in a ShortcutConflictWindow
+ if (parent != null && parent.GetType().Name == "ShortcutConflictWindow")
+ {
+ return ShortcutControlSource.ConflictWindow;
+ }
+
+ if (parent != null && (parent.GetType().Name == "MainWindow" || parent.GetType().Name == "ShellPage"))
+ {
+ return ShortcutControlSource.SettingsPage;
+ }
+ }
+
+ // Fallback to the explicitly set value or default
+ return ShortcutControlSource.ConflictWindow;
+ }
+
+ private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = d as ShortcutControl;
+ if (control == null)
+ {
+ return;
+ }
+
+ control.UpdateTooltip();
+ }
+
private ShortcutDialogContentControl c = new ShortcutDialogContentControl();
private ContentDialog shortcutDialog;
@@ -67,6 +149,24 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set => SetValue(AllowDisableProperty, value);
}
+ public bool HasConflict
+ {
+ get => (bool)GetValue(HasConflictProperty);
+ set => SetValue(HasConflictProperty, value);
+ }
+
+ public string Tooltip
+ {
+ get => (string)GetValue(TooltipProperty);
+ set => SetValue(TooltipProperty, value);
+ }
+
+ public ShortcutControlSource Source
+ {
+ get => (ShortcutControlSource)GetValue(SourceProperty);
+ set => SetValue(SourceProperty, value);
+ }
+
public bool Enabled
{
get
@@ -101,14 +201,54 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
if (hotkeySettings != value)
{
+ // Unsubscribe from old settings
+ if (hotkeySettings != null)
+ {
+ hotkeySettings.PropertyChanged -= OnHotkeySettingsPropertyChanged;
+ }
+
hotkeySettings = value;
SetValue(HotkeySettingsProperty, value);
+
+ // Subscribe to new settings
+ if (hotkeySettings != null)
+ {
+ hotkeySettings.PropertyChanged += OnHotkeySettingsPropertyChanged;
+
+ // Update UI based on conflict properties
+ UpdateConflictStatusFromHotkeySettings();
+ }
+
SetKeys();
- c.Keys = HotkeySettings.GetKeysList();
+ c.Keys = HotkeySettings?.GetKeysList();
}
}
}
+ private void OnHotkeySettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(HotkeySettings.HasConflict) ||
+ e.PropertyName == nameof(HotkeySettings.ConflictDescription))
+ {
+ UpdateConflictStatusFromHotkeySettings();
+ }
+ }
+
+ private void UpdateConflictStatusFromHotkeySettings()
+ {
+ if (hotkeySettings != null)
+ {
+ // Update the ShortcutControl's conflict properties from HotkeySettings
+ HasConflict = hotkeySettings.HasConflict;
+ Tooltip = hotkeySettings.HasConflict ? hotkeySettings.ConflictDescription : null;
+ }
+ else
+ {
+ HasConflict = false;
+ Tooltip = null;
+ }
+ }
+
public ShortcutControl()
{
InitializeComponent();
@@ -136,6 +276,29 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
OnAllowDisableChanged(this, null);
}
+ private void UpdateKeyVisualStyles()
+ {
+ if (PreviewKeysControl?.ItemsSource != null)
+ {
+ // Force refresh of the ItemsControl to update KeyVisual styles
+ var items = PreviewKeysControl.ItemsSource;
+ PreviewKeysControl.ItemsSource = null;
+ PreviewKeysControl.ItemsSource = items;
+ }
+ }
+
+ private void UpdateTooltip()
+ {
+ if (!string.IsNullOrEmpty(Tooltip))
+ {
+ ToolTipService.SetToolTip(EditButton, Tooltip);
+ }
+ else
+ {
+ ToolTipService.SetToolTip(EditButton, null);
+ }
+ }
+
private void ShortcutControl_Unloaded(object sender, RoutedEventArgs e)
{
shortcutDialog.PrimaryButtonClick -= ShortcutDialog_PrimaryButtonClick;
@@ -147,6 +310,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
App.GetSettingsWindow().Activated -= ShortcutDialog_SettingsWindow_Activated;
}
+ // Unsubscribe from HotkeySettings property changes
+ if (hotkeySettings != null)
+ {
+ hotkeySettings.PropertyChanged -= OnHotkeySettingsPropertyChanged;
+ }
+
// Dispose the HotkeySettingsControlHook object to terminate the hook threads when the textbox is unloaded
hook?.Dispose();
@@ -168,6 +337,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
App.GetSettingsWindow().Activated += ShortcutDialog_SettingsWindow_Activated;
}
+
+ // Initialize tooltip when loaded
+ UpdateTooltip();
}
private void KeyEventHandler(int key, bool matchValue, int matchValueCode)
@@ -302,6 +474,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
KeyEventHandler(key, true, key);
c.Keys = internalSettings.GetKeysList();
+ c.ConflictMessage = string.Empty;
+ c.HasConflict = false;
if (internalSettings.GetKeysList().Count == 0)
{
@@ -336,12 +510,74 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
else
{
EnableKeys();
+ if (lastValidSettings.IsValid())
+ {
+ if (string.Equals(lastValidSettings.ToString(), hotkeySettings.ToString(), StringComparison.OrdinalIgnoreCase))
+ {
+ c.HasConflict = hotkeySettings.HasConflict;
+ c.ConflictMessage = hotkeySettings.ConflictDescription;
+ }
+ else
+ {
+ // Check for conflicts with the new hotkey settings
+ CheckForConflicts(lastValidSettings);
+ }
+ }
}
}
c.IsWarningAltGr = internalSettings.Ctrl && internalSettings.Alt && !internalSettings.Win && (internalSettings.Code > 0);
}
+ private void CheckForConflicts(HotkeySettings settings)
+ {
+ void UpdateUIForConflict(bool hasConflict, HotkeyConflictResponse hotkeyConflictResponse)
+ {
+ DispatcherQueue.TryEnqueue(() =>
+ {
+ if (hasConflict)
+ {
+ // Build conflict message from all conflicts - only show module names
+ var conflictingModules = new HashSet();
+
+ foreach (var conflict in hotkeyConflictResponse.AllConflicts)
+ {
+ if (!string.IsNullOrEmpty(conflict.ModuleName))
+ {
+ conflictingModules.Add(conflict.ModuleName);
+ }
+ }
+
+ if (conflictingModules.Count > 0)
+ {
+ var moduleNames = conflictingModules.ToArray();
+ var conflictMessage = moduleNames.Length == 1
+ ? $"Conflict detected with {moduleNames[0]}"
+ : $"Conflicts detected with: {string.Join(", ", moduleNames)}";
+
+ c.ConflictMessage = conflictMessage;
+ }
+ else
+ {
+ c.ConflictMessage = "Conflict detected with unknown module";
+ }
+
+ c.HasConflict = true;
+ }
+ else
+ {
+ c.ConflictMessage = string.Empty;
+ c.HasConflict = false;
+ }
+ });
+ }
+
+ HotkeyConflictHelper.CheckHotkeyConflict(
+ settings,
+ ShellPage.SendDefaultIPCMessage,
+ UpdateUIForConflict);
+ }
+
private void EnableKeys()
{
shortcutDialog.IsPrimaryButtonEnabled = true;
@@ -416,6 +652,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
c.Keys = null;
c.Keys = HotkeySettings.GetKeysList();
+ c.HasConflict = hotkeySettings.HasConflict;
+ c.ConflictMessage = hotkeySettings.ConflictDescription;
+
// 92 means the Win key. The logic is: warning should be visible if the shortcut contains Alt AND contains Ctrl AND NOT contains Win.
// Additional key must be present, as this is a valid, previously used shortcut shown at dialog open. Check for presence of non-modifier-key is not necessary therefore
c.IsWarningAltGr = c.Keys.Contains("Ctrl") && c.Keys.Contains("Alt") && !c.Keys.Contains(92);
@@ -434,16 +673,32 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
lastValidSettings = hotkeySettings;
shortcutDialog.Hide();
+
+ // Send RequestAllConflicts IPC to update the UI after changed hotkey settings.
+ GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
}
private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (ComboIsValid(lastValidSettings))
{
- HotkeySettings = lastValidSettings with { };
+ if (c.HasConflict)
+ {
+ lastValidSettings = lastValidSettings with { HasConflict = true };
+ }
+ else
+ {
+ lastValidSettings = lastValidSettings with { HasConflict = false };
+ }
+
+ HotkeySettings = lastValidSettings;
}
SetKeys();
+
+ // Send RequestAllConflicts IPC to update the UI after changed hotkey settings.
+ GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
+
shortcutDialog.Hide();
}
@@ -520,7 +775,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
private void SetKeys()
{
- var keys = HotkeySettings.GetKeysList();
+ var keys = HotkeySettings?.GetKeysList();
if (keys != null && keys.Count > 0)
{
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml
index da982289e7..13033344ab 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml
@@ -63,6 +63,13 @@
IsOpen="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}"
IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}"
Severity="Warning" />
+
-
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs
index 5d44f7c451..8907f12415 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs
@@ -11,6 +11,24 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
public sealed partial class ShortcutDialogContentControl : UserControl
{
+ public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutDialogContentControl), new PropertyMetadata(default(string)));
+ public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
+ public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
+ public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
+ public static readonly DependencyProperty ConflictMessageProperty = DependencyProperty.Register("ConflictMessage", typeof(string), typeof(ShortcutDialogContentControl), new PropertyMetadata(string.Empty));
+
+ public bool HasConflict
+ {
+ get => (bool)GetValue(HasConflictProperty);
+ set => SetValue(HasConflictProperty, value);
+ }
+
+ public string ConflictMessage
+ {
+ get => (string)GetValue(ConflictMessageProperty);
+ set => SetValue(ConflictMessageProperty, value);
+ }
+
public ShortcutDialogContentControl()
{
this.InitializeComponent();
@@ -22,22 +40,16 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set { SetValue(KeysProperty, value); }
}
- public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
-
public bool IsError
{
get => (bool)GetValue(IsErrorProperty);
set => SetValue(IsErrorProperty, value);
}
- public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
-
public bool IsWarningAltGr
{
get => (bool)GetValue(IsWarningAltGrProperty);
set => SetValue(IsWarningAltGrProperty, value);
}
-
- public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml
index 78d95a4c3b..ea3be0bff8 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml
@@ -16,6 +16,7 @@
+ IsTabStop="False"
+ Style="{StaticResource DefaultKeyVisualStyle}" />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs
index ed18669eba..c3829e3984 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs
@@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set { SetValue(TextProperty, value); }
}
- public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string)));
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string)));
public List Keys
{
@@ -25,11 +25,40 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set { SetValue(KeysProperty, value); }
}
- public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string)));
+ public static readonly DependencyProperty KeysProperty = DependencyProperty.Register(nameof(Keys), typeof(List), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string)));
+
+ public LabelPlacement LabelPlacement
+ {
+ get { return (LabelPlacement)GetValue(LabelPlacementProperty); }
+ set { SetValue(LabelPlacementProperty, value); }
+ }
+
+ public static readonly DependencyProperty LabelPlacementProperty = DependencyProperty.Register(nameof(LabelPlacement), typeof(LabelPlacement), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(defaultValue: LabelPlacement.After, OnIsLabelPlacementChanged));
public ShortcutWithTextLabelControl()
{
this.InitializeComponent();
}
+
+ private static void OnIsLabelPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs newValue)
+ {
+ if (d is ShortcutWithTextLabelControl labelControl)
+ {
+ if (labelControl.LabelPlacement == LabelPlacement.Before)
+ {
+ VisualStateManager.GoToState(labelControl, "LabelBefore", true);
+ }
+ else
+ {
+ VisualStateManager.GoToState(labelControl, "LabelAfter", true);
+ }
+ }
+ }
+ }
+
+ public enum LabelPlacement
+ {
+ Before,
+ After,
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml
index b04c800bca..20815cd81c 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml
@@ -20,33 +20,56 @@
-
+
+ Margin="0,24,0,0"
+ Style="{StaticResource BodyStrongTextBlockStyle}" />
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
index ba4595ea06..baade0fb16 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
@@ -2,21 +2,32 @@
// 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 global::PowerToys.GPOWrapper;
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
+using Microsoft.PowerToys.Settings.UI.Services;
+using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry;
+using Microsoft.UI;
using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
- public sealed partial class OobeOverview : Page
+ public sealed partial class OobeOverview : Page, INotifyPropertyChanged
{
public OobePowerToysModule ViewModel { get; set; }
private bool _enableDataDiagnostics;
+ private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
+ private Windows.ApplicationModel.Resources.ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
public bool EnableDataDiagnostics
{
@@ -41,8 +52,151 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
}
}
+ public AllHotkeyConflictsData AllHotkeyConflictsData
+ {
+ get => _allHotkeyConflictsData;
+ set
+ {
+ if (_allHotkeyConflictsData != value)
+ {
+ _allHotkeyConflictsData = value;
+ OnPropertyChanged(nameof(AllHotkeyConflictsData));
+ OnPropertyChanged(nameof(ConflictCount));
+ OnPropertyChanged(nameof(ConflictText));
+ OnPropertyChanged(nameof(ConflictDescription));
+ OnPropertyChanged(nameof(HasConflicts));
+ OnPropertyChanged(nameof(IconGlyph));
+ OnPropertyChanged(nameof(IconForeground));
+ }
+ }
+ }
+
+ public int ConflictCount
+ {
+ get
+ {
+ if (AllHotkeyConflictsData == null)
+ {
+ return 0;
+ }
+
+ int count = 0;
+ if (AllHotkeyConflictsData.InAppConflicts != null)
+ {
+ count += AllHotkeyConflictsData.InAppConflicts.Count;
+ }
+
+ if (AllHotkeyConflictsData.SystemConflicts != null)
+ {
+ count += AllHotkeyConflictsData.SystemConflicts.Count;
+ }
+
+ return count;
+ }
+ }
+
+ public string ConflictText
+ {
+ get
+ {
+ var count = ConflictCount;
+ if (count == 0)
+ {
+ // Return no-conflict message
+ try
+ {
+ return resourceLoader.GetString("ShortcutConflictControl_NoConflictsFound");
+ }
+ catch
+ {
+ return "No conflicts found";
+ }
+ }
+ else if (count == 1)
+ {
+ // Try to get localized string
+ try
+ {
+ return resourceLoader.GetString("ShortcutConflictControl_SingleConflictFound");
+ }
+ catch
+ {
+ return "1 shortcut conflict";
+ }
+ }
+ else
+ {
+ // Try to get localized string
+ try
+ {
+ var template = resourceLoader.GetString("ShortcutConflictControl_MultipleConflictsFound");
+ return string.Format(System.Globalization.CultureInfo.CurrentCulture, template, count);
+ }
+ catch
+ {
+ return $"{count} shortcut conflicts";
+ }
+ }
+ }
+ }
+
+ public string ConflictDescription
+ {
+ get
+ {
+ var count = ConflictCount;
+ if (count == 0)
+ {
+ // Return no-conflict description
+ try
+ {
+ return resourceLoader.GetString("ShortcutConflictWindow_NoConflictsDescription");
+ }
+ catch
+ {
+ return "All shortcuts function correctly";
+ }
+ }
+ else
+ {
+ // Return conflict description
+ try
+ {
+ return resourceLoader.GetString("Oobe_Overview_Hotkey_Conflict_Card_Description");
+ }
+ catch
+ {
+ return "Shortcuts configured by PowerToys are conflicting";
+ }
+ }
+ }
+ }
+
+ public bool HasConflicts => ConflictCount > 0;
+
+ public string IconGlyph => HasConflicts ? "\uE814" : "\uE73E";
+
+ public SolidColorBrush IconForeground
+ {
+ get
+ {
+ if (HasConflicts)
+ {
+ // Red color for conflicts
+ return (SolidColorBrush)App.Current.Resources["SystemFillColorCriticalBrush"];
+ }
+ else
+ {
+ // Green color for no conflicts
+ return (SolidColorBrush)App.Current.Resources["SystemFillColorSuccessBrush"];
+ }
+ }
+ }
+
public bool ShowDataDiagnosticsSetting => GetIsDataDiagnosticsInfoBarEnabled();
+ public event PropertyChangedEventHandler PropertyChanged;
+
private bool GetIsDataDiagnosticsInfoBarEnabled()
{
var isDataDiagnosticsGpoDisallowed = GPOWrapper.GetAllowDataDiagnosticsValue() == GpoRuleConfigured.Disabled;
@@ -57,7 +211,27 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
_enableDataDiagnostics = DataDiagnosticsSettings.GetEnabledValue();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.Overview]);
- DataContext = ViewModel;
+ DataContext = this;
+
+ // Subscribe to hotkey conflict updates
+ if (GlobalHotkeyConflictManager.Instance != null)
+ {
+ GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
+ GlobalHotkeyConflictManager.Instance.RequestAllConflicts();
+ }
+ }
+
+ private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
+ {
+ this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
+ {
+ AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
+ });
+ }
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
@@ -80,6 +254,18 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
ViewModel.LogOpeningSettingsEvent();
}
+ private void ShortcutConflictBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ if (AllHotkeyConflictsData == null || !HasConflicts)
+ {
+ return;
+ }
+
+ // Create and show the shortcut conflict window
+ var conflictWindow = new ShortcutConflictWindow();
+ conflictWindow.Activate();
+ }
+
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
@@ -88,6 +274,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.LogClosingModuleEvent();
+
+ // Unsubscribe from conflict updates when leaving the page
+ if (GlobalHotkeyConflictManager.Instance != null)
+ {
+ GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
+ }
}
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml
index df622fe3e0..dbd347672f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml
@@ -11,141 +11,177 @@
Loaded="Page_Loaded"
mc:Ignorable="d">
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
index 1b2524eee8..15fcea6452 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
@@ -17,9 +18,11 @@ using CommunityToolkit.WinUI.UI.Controls;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
+using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml.Controls;
@@ -27,12 +30,54 @@ using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
- public sealed partial class OobeWhatsNew : Page
+ public sealed partial class OobeWhatsNew : Page, INotifyPropertyChanged
{
public OobePowerToysModule ViewModel { get; set; }
+ private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
+
public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar();
+ public AllHotkeyConflictsData AllHotkeyConflictsData
+ {
+ get => _allHotkeyConflictsData;
+ set
+ {
+ if (_allHotkeyConflictsData != value)
+ {
+ _allHotkeyConflictsData = value;
+ OnPropertyChanged(nameof(AllHotkeyConflictsData));
+ OnPropertyChanged(nameof(HasConflicts));
+ }
+ }
+ }
+
+ public bool HasConflicts
+ {
+ get
+ {
+ if (AllHotkeyConflictsData == null)
+ {
+ return false;
+ }
+
+ int count = 0;
+ if (AllHotkeyConflictsData.InAppConflicts != null)
+ {
+ count += AllHotkeyConflictsData.InAppConflicts.Count;
+ }
+
+ if (AllHotkeyConflictsData.SystemConflicts != null)
+ {
+ count += AllHotkeyConflictsData.SystemConflicts.Count;
+ }
+
+ return count > 0;
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
///
/// Initializes a new instance of the class.
///
@@ -40,7 +85,27 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
this.InitializeComponent();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.WhatsNew]);
- DataContext = ViewModel;
+ DataContext = this;
+
+ // Subscribe to hotkey conflict updates
+ if (GlobalHotkeyConflictManager.Instance != null)
+ {
+ GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
+ GlobalHotkeyConflictManager.Instance.RequestAllConflicts();
+ }
+ }
+
+ private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
+ {
+ this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
+ {
+ AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
+ });
+ }
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool GetShowDataDiagnosticsInfoBar()
@@ -184,6 +249,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.LogClosingModuleEvent();
+
+ // Unsubscribe from conflict updates when leaving the page
+ if (GlobalHotkeyConflictManager.Instance != null)
+ {
+ GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
+ }
}
private void ReleaseNotesMarkdown_LinkClicked(object sender, LinkClickedEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml
index 4a5f4233de..f277350fbc 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml
@@ -14,4 +14,4 @@
-
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
index a395ac767b..8442262688 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
@@ -31,6 +31,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs
index b38fffc59e..2e22da3120 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs
@@ -19,6 +19,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel = new AlwaysOnTopViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs
index a9a016b80e..fb3a97e309 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs
@@ -26,6 +26,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.SendDefaultIPCMessage,
DispatcherQueue);
DataContext = ViewModel;
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
InitializeComponent();
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs
index 37e6ffd47c..ce0f723633 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs
@@ -35,6 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
///
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs
index 66e3652da8..d769650dd1 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs
@@ -19,6 +19,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel = new CropAndLockViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml
index 80adc56c0b..e5b800cda1 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml
@@ -133,7 +133,7 @@
Grid.Column="1"
Orientation="Horizontal"
Spacing="16">
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs
index 2d6cf95bae..bf792e2b75 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs
@@ -39,6 +39,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel = new DashboardViewModel(
SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs
index c224c42683..61865c89fa 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs
@@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var settingsUtils = new SettingsUtils();
ViewModel = new FancyZonesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
index da5bcd7e0c..1a3c640ddf 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
@@ -267,7 +267,10 @@
-
+
ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml
index 0ba74ca164..01e9f8e740 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml
@@ -363,6 +363,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs
index 2a0cfa536f..ab3e8192ac 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs
@@ -48,6 +48,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
InitializeComponent();
this.MouseUtils_MouseJump_Panel.ViewModel = ViewModel;
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs
index f29056245f..a2e16ea987 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs
@@ -47,6 +47,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
DataContext = ViewModel;
InitializeComponent();
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
private void OnConfigFileUpdate()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs
index 24ca93208a..91adfa9a2e 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs
@@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
DispatcherQueue);
DataContext = ViewModel;
InitializeComponent();
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs
index f02327caa8..d8adcdc5a4 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs
@@ -40,6 +40,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
PowerLauncherSettings settings = SettingsRepository.GetInstance(settingsUtils)?.SettingsConfig;
ViewModel = new PowerLauncherViewModel(settings, SettingsRepository.GetInstance(settingsUtils), SendDefaultIPCMessageTimed, App.IsDarkTheme);
DataContext = ViewModel;
+
_ = Helper.GetFileWatcher(PowerLauncherSettings.ModuleName, "settings.json", () =>
{
if (Environment.TickCount < _lastIPCMessageSentTick + 500)
@@ -79,6 +80,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_ApplicationName"), "application_name"));
searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_StringInApplication"), "string_in_application"));
searchTypePreferencesOptions.Add(Tuple.Create(loader.GetString("PowerLauncher_SearchTypePreference_ExecutableName"), "executable_name"));
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs
index 07b999fce0..7acd547abe 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs
@@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
private void TextExtractor_ComboBox_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
index d1a4c8ea8b..0fc0f7c6a5 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml
@@ -1,4 +1,4 @@
-
-
-
-
-
-
+
-
-
-
+
@@ -309,5 +289,12 @@
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
index 4ed3faff9a..57abe04119 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation
+// 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.
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Windowing;
@@ -113,7 +114,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
///
/// Gets view model.
///
- public ShellViewModel ViewModel { get; } = new ShellViewModel();
+ public ShellViewModel ViewModel { get; }
///
/// Gets a collection of functions that handle IPC responses.
@@ -134,6 +135,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
InitializeComponent();
+ var settingsUtils = new SettingsUtils();
+ ViewModel = new ShellViewModel(SettingsRepository.GetInstance(settingsUtils));
DataContext = ViewModel;
ShellHandler = this;
ViewModel.Initialize(shellFrame, navigationView, KeyboardAccelerators);
@@ -141,6 +144,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// NL moved navigation to general page to the moment when the window is first activated (to not make flyout window disappear)
// shellFrame.Navigate(typeof(GeneralPage));
IPCResponseHandleList.Add(ReceiveMessage);
+ Services.IPCResponseService.Instance.RegisterForIPC();
SetTitleBar();
if (_navViewParentLookup.Count > 0)
@@ -460,17 +464,22 @@ namespace Microsoft.PowerToys.Settings.UI.Views
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
}
- private void ExitPTItem_Tapped(object sender, RoutedEventArgs e)
+ private async void Close_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
+ {
+ await CloseDialog.ShowAsync();
+ }
+
+ private void CloseDialog_Click(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
const string ptTrayIconWindowClass = "PToyTrayIconWindow"; // Defined in runner/tray_icon.h
- const nuint ID_EXIT_MENU_COMMAND = 40001; // Generated resource from runner/runner.base.rc
+ const nuint ID_CLOSE_MENU_COMMAND = 40001; // Generated resource from runner/runner.base.rc
// Exit the XAML application
Application.Current.Exit();
// Invoke the exit command from the tray icon
IntPtr hWnd = NativeMethods.FindWindow(ptTrayIconWindowClass, ptTrayIconWindowClass);
- NativeMethods.SendMessage(hWnd, NativeMethods.WM_COMMAND, ID_EXIT_MENU_COMMAND, 0);
+ NativeMethods.SendMessage(hWnd, NativeMethods.WM_COMMAND, ID_CLOSE_MENU_COMMAND, 0);
}
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs
index 750007595a..21b72f10ff 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs
@@ -20,6 +20,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var settingsUtils = new SettingsUtils();
ViewModel = new ShortcutGuideViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
+
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs
index 52814104c7..1c3905a406 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs
@@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ViewModel = new WorkspacesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
+ Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index c31076bb83..8834d22600 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -1,4 +1,4 @@
-
+