mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-06 12:27:01 +01:00
Compare commits
54 Commits
leilzh/wix
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43b2b4d5da | ||
|
|
5c48124e4c | ||
|
|
98063b17a7 | ||
|
|
eb35b3a249 | ||
|
|
5daec13bc4 | ||
|
|
ef6f4b2c3d | ||
|
|
336cdaff9b | ||
|
|
447118ab70 | ||
|
|
7455d63bb5 | ||
|
|
bb6b36af3f | ||
|
|
3ebf0f741a | ||
|
|
e8e1431e15 | ||
|
|
2d35fd8530 | ||
|
|
13d950a40a | ||
|
|
3eb8f96f3e | ||
|
|
11218ea4d8 | ||
|
|
8258f2ab6f | ||
|
|
4ad951eb56 | ||
|
|
64dc8e0f27 | ||
|
|
102865543d | ||
|
|
ef4e619350 | ||
|
|
7f3349b3f5 | ||
|
|
9c285856bf | ||
|
|
d90575b8da | ||
|
|
da36d410e3 | ||
|
|
a50d548a07 | ||
|
|
8c4a3a6944 | ||
|
|
efc68bc0c9 | ||
|
|
bf74bc43d4 | ||
|
|
ca4d811fa1 | ||
|
|
e10b7bd83a | ||
|
|
12537de422 | ||
|
|
76f5fabaa3 | ||
|
|
ea5f347a1a | ||
|
|
e842621036 | ||
|
|
56aa9acfb4 | ||
|
|
da572c6c40 | ||
|
|
69dc1d5e18 | ||
|
|
1a798e03cd | ||
|
|
8cb2e4eaf7 | ||
|
|
2db1dcd10c | ||
|
|
e0a0bbffe5 | ||
|
|
a5fe4b9e2e | ||
|
|
db953bb325 | ||
|
|
75d85f80b9 | ||
|
|
1e517f2721 | ||
|
|
df08d98a81 | ||
|
|
44d34e45c0 | ||
|
|
3c0af323bf | ||
|
|
e0097c94c6 | ||
|
|
e1086726ec | ||
|
|
759f5c02cb | ||
|
|
e0428eef1d | ||
|
|
3bc746d0ff |
19
.github/actions/spell-check/expect.txt
vendored
19
.github/actions/spell-check/expect.txt
vendored
@@ -32,6 +32,7 @@ AFeature
|
||||
affordances
|
||||
AFX
|
||||
AGGREGATABLE
|
||||
AHK
|
||||
AHybrid
|
||||
akv
|
||||
ALarger
|
||||
@@ -48,6 +49,7 @@ ALPHATYPE
|
||||
AModifier
|
||||
amr
|
||||
ANDSCANS
|
||||
animatedvisuals
|
||||
Animnate
|
||||
ANull
|
||||
AOC
|
||||
@@ -167,6 +169,7 @@ callbackptr
|
||||
calpwstr
|
||||
Cangjie
|
||||
CANRENAME
|
||||
Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
@@ -574,6 +577,7 @@ GPOCA
|
||||
gpp
|
||||
gpu
|
||||
gradians
|
||||
Gridcustomlayout
|
||||
GSM
|
||||
gtm
|
||||
guiddata
|
||||
@@ -668,6 +672,7 @@ HROW
|
||||
hsb
|
||||
HSCROLL
|
||||
hsi
|
||||
HSpeed
|
||||
HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
@@ -772,6 +777,7 @@ istep
|
||||
ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
IUWP
|
||||
IWIC
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
@@ -1006,6 +1012,7 @@ Mso
|
||||
msrc
|
||||
msstore
|
||||
msvcp
|
||||
MT
|
||||
MTND
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
@@ -1116,6 +1123,7 @@ NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
npmjs
|
||||
@@ -1494,6 +1502,7 @@ SETRULES
|
||||
SETSCREENSAVEACTIVE
|
||||
SETSTICKYKEYS
|
||||
SETTEXT
|
||||
settingscard
|
||||
SETTINGCHANGE
|
||||
SETTINGSCHANGED
|
||||
settingsheader
|
||||
@@ -1647,6 +1656,7 @@ STYLECHANGED
|
||||
STYLECHANGING
|
||||
subkeys
|
||||
sublang
|
||||
Subdomain
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
@@ -1750,11 +1760,13 @@ transcodetomp
|
||||
transicc
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
Tru
|
||||
trl
|
||||
trx
|
||||
tsa
|
||||
tskill
|
||||
tstoi
|
||||
tweakable
|
||||
TWF
|
||||
tymed
|
||||
TYPEKEYBOARD
|
||||
@@ -1862,6 +1874,7 @@ VSINSTALLDIR
|
||||
VSM
|
||||
vso
|
||||
vsonline
|
||||
VSpeed
|
||||
vstemplate
|
||||
vstest
|
||||
VSTHRD
|
||||
@@ -1998,10 +2011,13 @@ XNamespace
|
||||
Xoshiro
|
||||
XPels
|
||||
XPixel
|
||||
XPos
|
||||
XResource
|
||||
xsi
|
||||
XSpeed
|
||||
XStr
|
||||
xstyler
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
xxxxxx
|
||||
@@ -2011,7 +2027,10 @@ YIncrement
|
||||
yinle
|
||||
yinyue
|
||||
YPels
|
||||
YPos
|
||||
YResolution
|
||||
YSpeed
|
||||
YTimer
|
||||
YStr
|
||||
YVIRTUALSCREEN
|
||||
ZEROINIT
|
||||
|
||||
4
.github/actions/spell-check/patterns.txt
vendored
4
.github/actions/spell-check/patterns.txt
vendored
@@ -260,3 +260,7 @@ Process Process
|
||||
# ZoomIt menu items with accelerator keys
|
||||
E&xit
|
||||
St&yle
|
||||
|
||||
# This matches a relative clause where the relative pronoun "that" is omitted.
|
||||
# Example: "Gets or sets the window the TitleBar should configure."
|
||||
\bthe\s+\w+\s+the\b
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -355,5 +355,8 @@ src/common/Telemetry/*.etl
|
||||
# MSBuildCache
|
||||
/MSBuildCacheLogs/
|
||||
|
||||
# PowerToys Settings generated search index (legacy location) and obj outputs
|
||||
/src/settings-ui/Settings.UI/Assets/Settings/search.index.json
|
||||
|
||||
# PowerToysInstaller Build Temp Files
|
||||
installer/*/*.wxs.bk
|
||||
installer/*/*.wxs.bk
|
||||
@@ -28,6 +28,8 @@
|
||||
"PowerToys.GPOWrapperProjection.dll",
|
||||
"PowerToys.AllExperiments.dll",
|
||||
|
||||
"Common.Search.dll",
|
||||
|
||||
"PowerToys.AlwaysOnTop.exe",
|
||||
"PowerToys.AlwaysOnTopModuleInterface.dll",
|
||||
|
||||
@@ -280,6 +282,7 @@
|
||||
"Mono.Cecil.Pdb.dll",
|
||||
"Mono.Cecil.Rocks.dll",
|
||||
"Newtonsoft.Json.dll",
|
||||
"CommunityToolkit.WinUI.Controls.TitleBar.dll",
|
||||
|
||||
"NLog.dll",
|
||||
"HtmlAgilityPack.dll",
|
||||
|
||||
@@ -63,6 +63,7 @@ steps:
|
||||
msbuildArgs: >-
|
||||
/t:PowerToysSetupCustomActionsVNext
|
||||
/p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true
|
||||
/p:InstallerSuffix=${{ parameters.installerSuffix }}
|
||||
-restore -graph
|
||||
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-actions.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
@@ -94,6 +95,7 @@ steps:
|
||||
-restore
|
||||
/t:PowerToysInstallerVNext
|
||||
/p:RunBuildEvents=false;PerUser=${{parameters.buildUserInstaller}};BuildProjectReferences=false;CIBuild=true
|
||||
/p:InstallerSuffix=${{ parameters.installerSuffix }}
|
||||
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-msi.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
platform: $(BuildPlatform)
|
||||
@@ -140,6 +142,7 @@ steps:
|
||||
-restore
|
||||
/t:PowerToysBootstrapperVNext
|
||||
/p:PerUser=${{parameters.buildUserInstaller}};CIBuild=true
|
||||
/p:InstallerSuffix=${{ parameters.installerSuffix }}
|
||||
/bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-bootstrapper.binlog
|
||||
-restore -graph
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,7 +65,8 @@
|
||||
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="MSTest" Version="3.8.3" />
|
||||
<PackageVersion Include="NLog" Version="5.0.4" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="NLog" Version="5.2.8" />
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.0.0" />
|
||||
@@ -103,7 +104,7 @@
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="WinUIEx" Version="2.2.0" />
|
||||
<PackageVersion Include="WPF-UI" Version="3.0.5" />
|
||||
<PackageVersion Include="WyHash" Version="1.0.5" />
|
||||
@@ -121,4 +122,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
137
NOTICE.md
137
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 9.0.8
|
||||
- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
|
||||
- Microsoft.Data.Sqlite 9.0.8
|
||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||
- Microsoft.DotNet.ILCompiler (A)
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.8
|
||||
- Microsoft.Extensions.Hosting 9.0.8
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.8
|
||||
- Microsoft.Extensions.Logging 9.0.8
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.8
|
||||
- Microsoft.NET.ILLink.Tasks (A)
|
||||
- Microsoft.SemanticKernel 1.15.0
|
||||
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
|
||||
- Microsoft.Web.WebView2 1.0.2903.40
|
||||
- Microsoft.Win32.SystemEvents 9.0.8
|
||||
- Microsoft.Windows.Compatibility 9.0.8
|
||||
- Microsoft.Windows.CsWin32 0.3.183
|
||||
- Microsoft.Windows.CsWinRT 2.2.0
|
||||
- Microsoft.Windows.SDK.BuildTools 10.0.26100.4188
|
||||
- Microsoft.WindowsAppSDK 1.7.250513003
|
||||
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
|
||||
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
|
||||
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
|
||||
- ModernWpfUI 0.9.4
|
||||
- Moq 4.18.4
|
||||
- MSTest 3.8.3
|
||||
- NLog.Extensions.Logging 5.3.8
|
||||
- NLog.Schema 5.2.8
|
||||
- OpenAI 2.0.0
|
||||
- ReverseMarkdown 4.1.0
|
||||
- ScipBe.Common.Office.OneNote 3.0.1
|
||||
- SharpCompress 0.37.2
|
||||
- SkiaSharp.Views.WinUI 2.88.9
|
||||
- StreamJsonRpc 2.21.69
|
||||
- StyleCop.Analyzers 1.2.0-beta.556
|
||||
- System.CodeDom 9.0.8
|
||||
- System.CommandLine 2.0.0-beta4.22272.1
|
||||
- System.ComponentModel.Composition 9.0.8
|
||||
- System.Configuration.ConfigurationManager 9.0.8
|
||||
- System.Data.OleDb 9.0.8
|
||||
- System.Data.SqlClient 4.9.0
|
||||
- System.Diagnostics.EventLog 9.0.8
|
||||
- System.Diagnostics.PerformanceCounter 9.0.8
|
||||
- System.Drawing.Common 9.0.8
|
||||
- System.IO.Abstractions 22.0.13
|
||||
- System.IO.Abstractions.TestingHelpers 22.0.13
|
||||
- System.Management 9.0.8
|
||||
- System.Net.Http 4.3.4
|
||||
- System.Private.Uri 4.3.2
|
||||
- System.Reactive 6.0.1
|
||||
- System.Runtime.Caching 9.0.8
|
||||
- System.ServiceProcess.ServiceController 9.0.8
|
||||
- System.Text.Encoding.CodePages 9.0.8
|
||||
- System.Text.Json 9.0.8
|
||||
- System.Text.RegularExpressions 4.3.1
|
||||
- UnicodeInformation 2.6.0
|
||||
- UnitsNet 5.56.0
|
||||
- UTF.Unknown 2.5.1
|
||||
- WinUIEx 2.2.0
|
||||
- WPF-UI 3.0.5
|
||||
- WyHash 1.0.5
|
||||
- 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
|
||||
@@ -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
|
||||
@@ -711,6 +712,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Preview.BgcodePreviewHandle
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Preview.BgcodeThumbnailProvider.UnitTests", "src\modules\previewpane\UnitTests-BgcodeThumbnailProvider\Preview.BgcodeThumbnailProvider.UnitTests.csproj", "{61CBF221-9452-4934-B685-146285E080D7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Search", "src\common\Common.Search\Common.Search.csproj", "{38F187B2-6638-5A40-072F-DBE5E54070A0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Settings.UI.XamlIndexBuilder", "src\settings-ui\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj", "{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MouseUtils.UITests", "src\modules\MouseUtils\MouseUtils.UITests\MouseUtils.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Workspaces.Editor.UITests", "src\modules\Workspaces\WorkspacesEditorUITest\Workspaces.Editor.UITests.csproj", "{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}"
|
||||
@@ -788,6 +793,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
|
||||
@@ -2698,6 +2711,30 @@ Global
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.ActiveCfg = Release|x64
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.Build.0 = Release|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.Build.0 = Debug|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.ActiveCfg = Release|x64
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.Build.0 = Release|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.Build.0 = Debug|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.ActiveCfg = Release|x64
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.Build.0 = Release|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.Build.0 = Debug|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.ActiveCfg = Release|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.Build.0 = Release|x64
|
||||
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -2850,6 +2887,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
|
||||
@@ -2859,6 +2928,7 @@ Global
|
||||
{D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
|
||||
{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||
{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {264B412F-DB8B-4CF8-A74B-96998B183045}
|
||||
{1AFB6476-670D-4E80-A464-657E01DFF482} = {557C4636-D7E1-4838-A504-7D19B725EE95}
|
||||
{1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||
{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
|
||||
@@ -3126,6 +3196,8 @@ Global
|
||||
{61CBF221-9452-4934-B685-146285E080D7} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704}
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {2C318EC3-BA86-4372-B1BC-DB0F33C208B2}
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {68328142-5B31-4715-BCBB-7B6345EE0971}
|
||||
{38F187B2-6638-5A40-072F-DBE5E54070A0} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{DA0744BC-E822-680E-9CEB-D0FBA903A8EE} = {C3081D9A-1586-441A-B5F4-ED815B3719C1}
|
||||
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{14AFD976-B4D2-49D0-9E6C-AA93CC061B8A} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{9D3F3793-EFE3-4525-8782-238015DABA62} = {66E1534A-1587-42B2-912F-45C994D32904}
|
||||
@@ -3161,6 +3233,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}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 18 KiB |
BIN
doc/specs/search-result.png
Normal file
BIN
doc/specs/search-result.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
233
doc/specs/settings-search.md
Normal file
233
doc/specs/settings-search.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# PowerToys Settings – Search Index (Hard-sealed)
|
||||
|
||||
## 1. What to index
|
||||
|
||||
This section describes the current structure of the settings pages in PowerToys. All user-facing settings are contained in the content of <controls:SettingsPageControl>. The logical and visual structure of settings follows a nested layout as shown below:
|
||||
|
||||
```css
|
||||
SettingsPageControl
|
||||
└─ SettingsGroup
|
||||
└─ [SettingsExpander]
|
||||
└─ SettingsCard
|
||||
```
|
||||
* Each SettingsGroup defines a functional section within a settings page.
|
||||
|
||||
* An optional SettingsExpander may be used to further organize related settings inside a group.
|
||||
|
||||
* Each actual setting is represented by a SettingsCard, which contains one user-tweakable control or a group of closely related controls.
|
||||
|
||||
>Note: Not all SettingsCard are necessarily wrapped in a SettingsExpander; they can exist directly under a SettingsGroup.
|
||||
|
||||
> For indexing purposes, we are specifically targeting all SettingsCard elements. These are the smallest units of user interaction and correspond to individual configurable settings.
|
||||
|
||||
> There could be setting item in expander, so we also need to index expander items as well.
|
||||
|
||||
### Module
|
||||
Module is a primary type that needs to be indexed, for modules, we need to index the 'ModuleTitle' and the 'ModuleDescription'.
|
||||
So these two should be passed in by x:Uid and binding with a key.
|
||||
|
||||
|
||||
### SettingsCard/SettingsExpander
|
||||
|
||||
Each SettingsCard/SettingsExpander should have an x:Uid for localization and indexing. The associated display strings are defined in the .resw files:
|
||||
|
||||
{x:Uid}.Header – The visible label/title of the setting.
|
||||
{x:Uid}.Description – (optional) The tooltip or explanatory text.
|
||||
|
||||
The index should be built around these SettingsCard elements and their x:Uid-bound resources, as they represent the actual settings users will search for.
|
||||
|
||||
---
|
||||
|
||||
## 2. How to Navigate
|
||||
|
||||
### Entry
|
||||
```csharp
|
||||
enum EntryType
|
||||
{
|
||||
SettingsPage,
|
||||
SettingsCard,
|
||||
SettingsExpander,
|
||||
}
|
||||
|
||||
public class SearchableElementMetadata
|
||||
{
|
||||
public string PageName { get; set; } // Used to navigate to a specific page
|
||||
public EntryType Type { get; set; } // Used to know how should we navigate(As a page, a settingscard or an expander?)
|
||||
public string ParentElementName { get; set; }
|
||||
public string ElementName { get; set; }
|
||||
public string ElementUid { get; set; }
|
||||
public string Icon { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation
|
||||
The steps for navigate to an item:
|
||||
* Navigate among pages
|
||||
* [optional] Expand the expander if setting entry is inside an expander
|
||||
* [optional] Navigate within page
|
||||
|
||||
> Use page name for navigation:
|
||||
```csharp
|
||||
Type GetPageTypeFromPageName(string PageName)
|
||||
{
|
||||
var assembly = typeof(GeneralPage).Assembly;
|
||||
return assembly.GetType($"Microsoft.PowerToys.Settings.UI.Views.{PageName}");
|
||||
}
|
||||
|
||||
NavigationService.Navigate(PageType, ElementName,ParentElementName);
|
||||
```
|
||||
|
||||
> Use ElementName and ParentElementName for in page navigation:
|
||||
```csharp
|
||||
Page.OnNavigateTo(ElementName, ParentElementName){
|
||||
var element = this.FindName(name) as FrameworkElement;
|
||||
var parentElement = this.FindName(ParentElementName) as FrameworkElement;
|
||||
|
||||
if(parentElement) {
|
||||
expander = (Expander)parentElement;
|
||||
if(expander){
|
||||
expander.Expand();
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.startbringintoview?view=winrt-26100
|
||||
element.StartBringIntoView();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Runtime Search
|
||||
When user start typing for an entry, e.g. shortcut or 快捷键(cn version of shortcut),
|
||||
we need to go through all the entries to see if an entry matches the search text.
|
||||
|
||||
A naive approach will be try to match all the localized text one by one and see if they match.
|
||||
Total entry is within thousand(To fill in an exact number), performance is acceptable now.
|
||||
```csharp
|
||||
// Match
|
||||
query = UserInput();
|
||||
matched = {};
|
||||
|
||||
indexes = BuildIndex();
|
||||
|
||||
foreach(var entry in indexes) {
|
||||
if(entry.Match(query)) {
|
||||
matched.Add(entry);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And we don't intend to introduce complexity on the match algorithm discussion, so let's use powertoys FuzzMatch impl for now.
|
||||
```csharp
|
||||
MatchResult Match(this Entry entry, string query) {
|
||||
return FuzzMatch(entry.DisplayedText, query);
|
||||
}
|
||||
|
||||
struct MatchResult{
|
||||
int Score;
|
||||
bool Result;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Search Result Page
|
||||

|
||||
After we got matched items, map these items to a search result page according to spec.
|
||||
```c#
|
||||
ObservableCollection<SettingEntry> ModuleResult;
|
||||
ObservableCollection<SettingsGroup> GroupedSettingsResults;
|
||||
|
||||
public class SettingsGroup : INotifyPropertyChanged
|
||||
{
|
||||
private string _groupName;
|
||||
private ObservableCollection<SettingEntry> _settings;
|
||||
public string GroupName
|
||||
{
|
||||
get => _groupName;
|
||||
set
|
||||
{
|
||||
_groupName = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public ObservableCollection<SettingEntry> Settings
|
||||
{
|
||||
get => _settings;
|
||||
set
|
||||
{
|
||||
_settings = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. How to do Index
|
||||
### Runtime index or build time index?
|
||||
Now We need to build all the entries in our settings.
|
||||
|
||||
Most of the entry properties are static, and in runtime, the `SettingsCard` is compiled into native winUI3 controls <small>(I suppose, please correct here if it's wrong)</small>, it's hard to locate all the `SettingsCard`, and performance is terrible if we do search for all the pages' elements.
|
||||
|
||||
### Build time indexing
|
||||
We can rely on xaml file parsing to get all the SettingsCard Entries.
|
||||
And we don't want xaml file to be brought into production bundle.
|
||||
Use a project for parsing and bring that index file into production bundle is a solution.
|
||||
```csproj
|
||||
<Target Name="GenerateSearchIndex" BeforeTargets="BeforeBuild">
|
||||
<PropertyGroup>
|
||||
<BuilderExe>$(MSBuildProjectDirectory)\..\Settings.UI.XamlIndexBuilder\bin\$(Configuration)\net8.0\XamlIndexBuilder.exe</BuilderExe>
|
||||
<XamlDir>$(MSBuildProjectDirectory)\Views</XamlDir>
|
||||
<GeneratedJson>$(MSBuildProjectDirectory)\Services\searchable_elements.json</GeneratedJson>
|
||||
</PropertyGroup>
|
||||
<Exec Command=""$(BuilderExe)" "$(XamlDir)" "$(GeneratedJson)"" />
|
||||
</Target>
|
||||
```
|
||||
```csharp
|
||||
for(xamlFile in xamlFiles){
|
||||
var doc = Load(xamlFile);
|
||||
var elements = doc.Descendants();
|
||||
|
||||
foreach(var element in elements){
|
||||
if(element.Name == "SettingsCard") {
|
||||
var entry = new Entry{
|
||||
ElementName = element.Attribute["Name"],
|
||||
PageName = FileName,
|
||||
Type = "SettingsCard",
|
||||
ElementUid = element.Attribute["Uid"],
|
||||
DisplayedText = "",
|
||||
}
|
||||
|
||||
var parent = element.GetParent();
|
||||
if(parent.Name == "SettingsExpander"){
|
||||
entry.ParentElementName = parent.Attribute["Name"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Runtime index loading:
|
||||
```
|
||||
var entries = LoadEntriesFromFile();
|
||||
foreach(var entry in entries){
|
||||
entry.DisplayedText = ResourceLoader.GetString(entry.Uid);
|
||||
}
|
||||
```
|
||||
So now we have all the entries and entry properties.
|
||||
|
||||
## Overall flow:
|
||||
|
||||

|
||||
|
||||
|
||||
## 6. Corner cases - that have not addressed yet
|
||||
|
||||
1. CmdPal page is not in scope of this effort, that needs additional effort&design to launch and search within cmdpal settings page.
|
||||
|
||||
2. Go back button
|
||||
|
||||
3. Dynamic constructed settings page
|
||||
- Shortcut guide, with visibility converter
|
||||
- advanced paste dynamically configured setting items
|
||||
- powertoys run's extensions
|
||||
BIN
doc/specs/workflow.png
Normal file
BIN
doc/specs/workflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@@ -14,21 +14,6 @@
|
||||
<DirectoryRef Id="FileLocksmithAssetsInstallFolder" FileSource="$(var.FileLocksmithAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--FileLocksmithAssetsFiles_Component_Def-->
|
||||
<!-- !Warning! Make sure to change Component Guid if you update something here -->
|
||||
<Component Id="Module_FileLocksmith" Guid="108D3EC1-E6E0-4E81-88EF-25966133CB41" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{84D68575-E186-46AD-B0CB-BAEB45EE29C0}">
|
||||
<RegistryValue Type="string" Value="File Locksmith Shell Extension" />
|
||||
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.FileLocksmithExt.dll" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\AllFileSystemObjects\ShellEx\ContextMenuHandlers\FileLocksmithExt">
|
||||
<RegistryValue Type="string" Value="{84D68575-E186-46AD-B0CB-BAEB45EE29C0}"/>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\Drive\ShellEx\ContextMenuHandlers\FileLocksmithExt">
|
||||
<RegistryValue Type="string" Value="{84D68575-E186-46AD-B0CB-BAEB45EE29C0}"/>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="FileLocksmithComponentGroup">
|
||||
@@ -38,7 +23,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderFileLocksmithAssetsFolder" Directory="FileLocksmithAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_FileLocksmith" />
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -16,71 +16,6 @@
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--ImageResizerAssetsFiles_Component_Def-->
|
||||
|
||||
<Component Id="Module_ImageResizer_Registry" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{51B4D7E5-7568-4234-B4BB-47FB3C016A69}\InprocServer32">
|
||||
<RegistryValue Value="[WinUI3AppsInstallFolder]PowerToys.ImageResizerExt.dll" Type="string" />
|
||||
<RegistryValue Name="ThreadingModel" Value="Apartment" Type="string" />
|
||||
</RegistryKey>
|
||||
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\Directory\ShellEx\DragDropHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<!-- Registry Keys for the context menu handler for each of the following image formats: bmp, dib, gif, jfif, jpe, jpeg, jpg, jxr, png, rle, tif, tiff, wdp -->
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.bmp\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.dib\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.gif\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jfif\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jpe\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jpeg\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jpg\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.jxr\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.png\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.rle\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.tif\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.tiff\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
<RegistryValue Root="$(var.RegistryScope)"
|
||||
Key="SOFTWARE\Classes\SystemFileAssociations\.wdp\ShellEx\ContextMenuHandlers\ImageResizer"
|
||||
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
|
||||
Type="string" />
|
||||
</Component>
|
||||
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="ImageResizerComponentGroup">
|
||||
@@ -90,7 +25,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderImageResizerAssetsFolder" Directory="ImageResizerAssetsFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_ImageResizer_Registry" />
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -18,19 +18,6 @@
|
||||
<DirectoryRef Id="NewPlusAssetsInstallFolder" FileSource="$(var.NewPlusAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--NewPlusAssetsFiles_Component_Def-->
|
||||
|
||||
<!-- NewPlus Shell Extension for Win10 registration -->
|
||||
<Component Id="NewPlus_ShellExtension_win10" Guid="D5456D4A-6EEC-4B85-944D-6A6A4A74FFA6" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{FF90D477-E32A-4BE8-8CC5-A502A97F5401}">
|
||||
<RegistryValue Type="string" Value="NewPlus Shell Extension Win10" />
|
||||
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.NewPlus.ShellExtension.win10.dll" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\Directory\background\ShellEx\ContextMenuHandlers\NewPlusShellExtensionWin10">
|
||||
<RegistryValue Type="string" Value="{FF90D477-E32A-4BE8-8CC5-A502A97F5401}"/>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="NewPlusComponentGroup">
|
||||
@@ -40,8 +27,7 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderNewPlusAssetsFolder" Directory="NewPlusAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="NewPlus_ShellExtension_win10" />
|
||||
</ComponentGroup>
|
||||
</ComponentGroup>
|
||||
|
||||
|
||||
<!-- Example templates -->
|
||||
@@ -81,7 +67,7 @@
|
||||
</Component>
|
||||
<ComponentRef Id="NewPlusTemplateFiles_Component" />
|
||||
<ComponentRef Id="NewPlusTemplateSubFiles_Component" />
|
||||
</ComponentGroup>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -14,22 +14,6 @@
|
||||
<DirectoryRef Id="PowerRenameAssetsFolder" FileSource="$(var.PowerRenameAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerRenameAssetsFiles_Component_Def-->
|
||||
<!-- !Warning! Make sure to change Component Guid if you update something here -->
|
||||
<Component Id="Module_PowerRename" Guid="40D43079-240E-402D-8CE8-571BFFA71175" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{0440049F-D1DC-4E46-B27B-98393D79486B}">
|
||||
<RegistryValue Type="string" Value="PowerRename Shell Extension" />
|
||||
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.PowerRenameExt.dll" />
|
||||
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\AllFileSystemObjects\ShellEx\ContextMenuHandlers\PowerRenameExt">
|
||||
<RegistryValue Type="string" Value="{0440049F-D1DC-4E46-B27B-98393D79486B}"/>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\Directory\background\ShellEx\ContextMenuHandlers\PowerRenameExt">
|
||||
<RegistryValue Type="string" Value="{0440049F-D1DC-4E46-B27B-98393D79486B}"/>
|
||||
</RegistryKey>
|
||||
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerRenameComponentGroup">
|
||||
@@ -39,7 +23,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerRenameAssetsFolder" Directory="PowerRenameAssetsFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_PowerRename" />
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -176,6 +176,18 @@
|
||||
<Custom Action="UnRegisterContextMenuPackages" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="CleanImageResizerRuntimeRegistry" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="CleanFileLocksmithRuntimeRegistry" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="CleanPowerRenameRuntimeRegistry" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="CleanNewPlusRuntimeRegistry" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles">
|
||||
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
@@ -437,6 +449,35 @@
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="UnRegisterContextMenuPackagesCA"
|
||||
/>
|
||||
|
||||
<CustomAction Id="CleanImageResizerRuntimeRegistry"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="CleanImageResizerRuntimeRegistryCA"
|
||||
/>
|
||||
<CustomAction Id="CleanFileLocksmithRuntimeRegistry"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="CleanFileLocksmithRuntimeRegistryCA"
|
||||
/>
|
||||
<CustomAction Id="CleanPowerRenameRuntimeRegistry"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="CleanPowerRenameRuntimeRegistryCA"
|
||||
/>
|
||||
<CustomAction Id="CleanNewPlusRuntimeRegistry"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
DllEntry="CleanNewPlusRuntimeRegistryCA"
|
||||
/>
|
||||
|
||||
<CustomAction Id="UnRegisterCmdPalPackage"
|
||||
|
||||
@@ -1153,6 +1153,113 @@ UINT __stdcall UnRegisterContextMenuPackagesCA(MSIHANDLE hInstall)
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall CleanImageResizerRuntimeRegistryCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
hr = WcaInitialize(hInstall, "CleanImageResizerRuntimeRegistryCA");
|
||||
|
||||
try
|
||||
{
|
||||
const wchar_t* CLSID_STR = L"{51B4D7E5-7568-4234-B4BB-47FB3C016A69}";
|
||||
const wchar_t* exts[] = { L".bmp", L".dib", L".gif", L".jfif", L".jpe", L".jpeg", L".jpg", L".jxr", L".png", L".rle", L".tif", L".tiff", L".wdp" };
|
||||
|
||||
auto deleteKeyRecursive = [](HKEY root, const std::wstring &path) {
|
||||
RegDeleteTreeW(root, path.c_str());
|
||||
};
|
||||
|
||||
// InprocServer32 chain root CLSID
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
|
||||
// DragDrop handler
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\ShellEx\\DragDropHandlers\\ImageResizer");
|
||||
// Extensions
|
||||
for (auto ext : exts)
|
||||
{
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\SystemFileAssociations\\" + std::wstring(ext) + L"\\ShellEx\\ContextMenuHandlers\\ImageResizer");
|
||||
}
|
||||
// Sentinel
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\ImageResizer");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
er = ERROR_INSTALL_FAILURE;
|
||||
}
|
||||
|
||||
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall CleanFileLocksmithRuntimeRegistryCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
hr = WcaInitialize(hInstall, "CleanFileLocksmithRuntimeRegistryCA");
|
||||
try
|
||||
{
|
||||
const wchar_t* CLSID_STR = L"{84D68575-E186-46AD-B0CB-BAEB45EE29C0}";
|
||||
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
|
||||
RegDeleteTreeW(root, path.c_str());
|
||||
};
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt");
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Drive\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\FileLocksmith");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
er = ERROR_INSTALL_FAILURE;
|
||||
}
|
||||
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall CleanPowerRenameRuntimeRegistryCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
hr = WcaInitialize(hInstall, "CleanPowerRenameRuntimeRegistryCA");
|
||||
try
|
||||
{
|
||||
const wchar_t* CLSID_STR = L"{0440049F-D1DC-4E46-B27B-98393D79486B}";
|
||||
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
|
||||
RegDeleteTreeW(root, path.c_str());
|
||||
};
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\PowerRenameExt");
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\PowerRenameExt");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\PowerRename");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
er = ERROR_INSTALL_FAILURE;
|
||||
}
|
||||
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall CleanNewPlusRuntimeRegistryCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
hr = WcaInitialize(hInstall, "CleanNewPlusRuntimeRegistryCA");
|
||||
try
|
||||
{
|
||||
const wchar_t* CLSID_STR = L"{FF90D477-E32A-4BE8-8CC5-A502A97F5401}";
|
||||
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
|
||||
RegDeleteTreeW(root, path.c_str());
|
||||
};
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
|
||||
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\NewPlusShellExtensionWin10");
|
||||
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\NewPlus");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
er = ERROR_INSTALL_FAILURE;
|
||||
}
|
||||
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
|
||||
@@ -28,3 +28,7 @@ EXPORTS
|
||||
UninstallCommandNotFoundModuleCA
|
||||
UpgradeCommandNotFoundModuleCA
|
||||
UnsetAdvancedPasteAPIKeyCA
|
||||
CleanImageResizerRuntimeRegistryCA
|
||||
CleanFileLocksmithRuntimeRegistryCA
|
||||
CleanPowerRenameRuntimeRegistryCA
|
||||
CleanNewPlusRuntimeRegistryCA
|
||||
|
||||
@@ -7,7 +7,7 @@ import { srtDefinition } from './customLanguages/srt.js';
|
||||
export async function registerAdditionalLanguages(monaco){
|
||||
await languageDefinitions();
|
||||
registerAdditionalLanguage("cppExt", [".ino", ".pde"], "cpp", monaco);
|
||||
registerAdditionalLanguage("xmlExt", [".wsdl", ".csproj", ".vcxproj", ".vbproj", ".fsproj", ".resx", ".resw"], "xml", monaco);
|
||||
registerAdditionalLanguage("xmlExt", [".wsdl", ".projitems", ".csproj", ".fsproj", ".shproj", ".vcxproj", ".vbproj", ".resx", ".resw"], "xml", monaco);
|
||||
registerAdditionalLanguage("txtExt", [".sln", ".log", ".vsconfig", ".env", ".ahk", ".ion"], "txt", monaco);
|
||||
registerAdditionalLanguage("razorExt", [".razor"], "razor", monaco);
|
||||
registerAdditionalLanguage("vbExt", [".vbs"], "vb", monaco);
|
||||
|
||||
File diff suppressed because one or more lines are too long
8
src/common/Common.Search/Common.Search.csproj
Normal file
8
src/common/Common.Search/Common.Search.csproj
Normal file
@@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
10
src/common/Common.Search/FuzzSearch/MatchOption.cs
Normal file
10
src/common/Common.Search/FuzzSearch/MatchOption.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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 Common.Search.FuzzSearch;
|
||||
|
||||
public class MatchOption
|
||||
{
|
||||
public bool IgnoreCase { get; set; } = true;
|
||||
}
|
||||
67
src/common/Common.Search/FuzzSearch/MatchResult.cs
Normal file
67
src/common/Common.Search/FuzzSearch/MatchResult.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Common.Search.FuzzSearch;
|
||||
|
||||
public class MatchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The raw calculated search score without any search precision filtering applied.
|
||||
/// </summary>
|
||||
private int _rawScore;
|
||||
|
||||
public MatchResult(bool success, SearchPrecisionScore searchPrecision)
|
||||
{
|
||||
Success = success;
|
||||
SearchPrecision = searchPrecision;
|
||||
}
|
||||
|
||||
public MatchResult(bool success, SearchPrecisionScore searchPrecision, List<int> matchData, int rawScore)
|
||||
{
|
||||
Success = success;
|
||||
SearchPrecision = searchPrecision;
|
||||
MatchData = matchData;
|
||||
RawScore = rawScore;
|
||||
}
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the final score of the match result with search precision filters applied.
|
||||
/// </summary>
|
||||
public int Score { get; private set; }
|
||||
|
||||
public int RawScore
|
||||
{
|
||||
get => _rawScore;
|
||||
|
||||
set
|
||||
{
|
||||
_rawScore = value;
|
||||
Score = ScoreAfterSearchPrecisionFilter(_rawScore);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets matched data to highlight.
|
||||
/// </summary>
|
||||
public List<int> MatchData { get; private set; } = new();
|
||||
|
||||
public SearchPrecisionScore SearchPrecision { get; set; }
|
||||
|
||||
public bool IsSearchPrecisionScoreMet()
|
||||
{
|
||||
return IsSearchPrecisionScoreMet(_rawScore);
|
||||
}
|
||||
|
||||
private bool IsSearchPrecisionScoreMet(int rawScore)
|
||||
{
|
||||
return rawScore >= (int)SearchPrecision;
|
||||
}
|
||||
|
||||
private int ScoreAfterSearchPrecisionFilter(int rawScore)
|
||||
{
|
||||
return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0;
|
||||
}
|
||||
}
|
||||
12
src/common/Common.Search/FuzzSearch/SearchPrecisionScore.cs
Normal file
12
src/common/Common.Search/FuzzSearch/SearchPrecisionScore.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// 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 Common.Search.FuzzSearch;
|
||||
|
||||
public enum SearchPrecisionScore
|
||||
{
|
||||
Regular = 50,
|
||||
Low = 20,
|
||||
None = 0,
|
||||
}
|
||||
272
src/common/Common.Search/FuzzSearch/StringMatcher.cs
Normal file
272
src/common/Common.Search/FuzzSearch/StringMatcher.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
// 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;
|
||||
|
||||
namespace Common.Search.FuzzSearch;
|
||||
|
||||
public class StringMatcher
|
||||
{
|
||||
public StringMatcher()
|
||||
{
|
||||
}
|
||||
|
||||
private static readonly char[] Separator = [' '];
|
||||
|
||||
/// <summary>
|
||||
/// Current method:
|
||||
/// Character matching + substring matching;
|
||||
/// 1. Query search string is split into substrings, separator is whitespace.
|
||||
/// 2. Check each query substring's characters against full compare string,
|
||||
/// 3. if a character in the substring is matched, loop back to verify the previous character.
|
||||
/// 4. If previous character also matches, and is the start of the substring, update list.
|
||||
/// 5. Once the previous character is verified, move on to the next character in the query substring.
|
||||
/// 6. Move onto the next substring's characters until all substrings are checked.
|
||||
/// 7. Consider success and move onto scoring if every char or substring without whitespaces matched
|
||||
/// </summary>
|
||||
public static MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt = null)
|
||||
{
|
||||
opt = opt ?? new MatchOption();
|
||||
|
||||
if (string.IsNullOrEmpty(stringToCompare))
|
||||
{
|
||||
return new MatchResult(false, SearchPrecisionScore.Regular);
|
||||
}
|
||||
|
||||
SearchPrecisionScore score = SearchPrecisionScore.Regular;
|
||||
|
||||
var bestResult = new MatchResult(false, score);
|
||||
|
||||
for (int startIndex = 0; startIndex < stringToCompare.Length; startIndex++)
|
||||
{
|
||||
MatchResult result = FuzzyMatch(query, stringToCompare, opt, startIndex);
|
||||
if (result.Success && (!bestResult.Success || result.Score > bestResult.Score))
|
||||
{
|
||||
bestResult = result;
|
||||
}
|
||||
}
|
||||
|
||||
return bestResult;
|
||||
}
|
||||
|
||||
private static MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt, int startIndex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query))
|
||||
{
|
||||
return new MatchResult(false, SearchPrecisionScore.Regular);
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(opt);
|
||||
|
||||
query = query.Trim();
|
||||
|
||||
// Using InvariantCulture since this is internal
|
||||
var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToUpper(CultureInfo.InvariantCulture) : stringToCompare;
|
||||
var queryWithoutCase = opt.IgnoreCase ? query.ToUpper(CultureInfo.InvariantCulture) : query;
|
||||
|
||||
var querySubstrings = queryWithoutCase.Split(Separator, StringSplitOptions.RemoveEmptyEntries);
|
||||
int currentQuerySubstringIndex = 0;
|
||||
var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
|
||||
var currentQuerySubstringCharacterIndex = 0;
|
||||
|
||||
var firstMatchIndex = -1;
|
||||
var firstMatchIndexInWord = -1;
|
||||
var lastMatchIndex = 0;
|
||||
bool allQuerySubstringsMatched = false;
|
||||
bool matchFoundInPreviousLoop = false;
|
||||
bool allSubstringsContainedInCompareString = true;
|
||||
|
||||
var indexList = new List<int>();
|
||||
List<int> spaceIndices = new List<int>();
|
||||
|
||||
for (var compareStringIndex = startIndex; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++)
|
||||
{
|
||||
// To maintain a list of indices which correspond to spaces in the string to compare
|
||||
// To populate the list only for the first query substring
|
||||
if (fullStringToCompareWithoutCase[compareStringIndex].Equals(' ') && currentQuerySubstringIndex == 0)
|
||||
{
|
||||
spaceIndices.Add(compareStringIndex);
|
||||
}
|
||||
|
||||
bool compareResult;
|
||||
if (opt.IgnoreCase)
|
||||
{
|
||||
var fullStringToCompare = fullStringToCompareWithoutCase[compareStringIndex].ToString();
|
||||
var querySubstring = currentQuerySubstring[currentQuerySubstringCharacterIndex].ToString();
|
||||
#pragma warning disable CA1309 // Use ordinal string comparison (We are looking for a fuzzy match here)
|
||||
compareResult = string.Compare(fullStringToCompare, querySubstring, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) != 0;
|
||||
#pragma warning restore CA1309 // Use ordinal string comparison
|
||||
}
|
||||
else
|
||||
{
|
||||
compareResult = fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex];
|
||||
}
|
||||
|
||||
if (compareResult)
|
||||
{
|
||||
matchFoundInPreviousLoop = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstMatchIndex < 0)
|
||||
{
|
||||
// first matched char will become the start of the compared string
|
||||
firstMatchIndex = compareStringIndex;
|
||||
}
|
||||
|
||||
if (currentQuerySubstringCharacterIndex == 0)
|
||||
{
|
||||
// first letter of current word
|
||||
matchFoundInPreviousLoop = true;
|
||||
firstMatchIndexInWord = compareStringIndex;
|
||||
}
|
||||
else if (!matchFoundInPreviousLoop)
|
||||
{
|
||||
// we want to verify that there is not a better match if this is not a full word
|
||||
// in order to do so we need to verify all previous chars are part of the pattern
|
||||
var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex;
|
||||
|
||||
if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, fullStringToCompareWithoutCase, currentQuerySubstring))
|
||||
{
|
||||
matchFoundInPreviousLoop = true;
|
||||
|
||||
// if it's the beginning character of the first query substring that is matched then we need to update start index
|
||||
firstMatchIndex = currentQuerySubstringIndex == 0 ? startIndexToVerify : firstMatchIndex;
|
||||
|
||||
indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, firstMatchIndexInWord, indexList);
|
||||
}
|
||||
}
|
||||
|
||||
lastMatchIndex = compareStringIndex + 1;
|
||||
indexList.Add(compareStringIndex);
|
||||
|
||||
currentQuerySubstringCharacterIndex++;
|
||||
|
||||
// if finished looping through every character in the current substring
|
||||
if (currentQuerySubstringCharacterIndex == currentQuerySubstring.Length)
|
||||
{
|
||||
// if any of the substrings was not matched then consider as all are not matched
|
||||
allSubstringsContainedInCompareString = matchFoundInPreviousLoop && allSubstringsContainedInCompareString;
|
||||
|
||||
currentQuerySubstringIndex++;
|
||||
|
||||
allQuerySubstringsMatched = AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length);
|
||||
if (allQuerySubstringsMatched)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// otherwise move to the next query substring
|
||||
currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
|
||||
currentQuerySubstringCharacterIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// proceed to calculate score if every char or substring without whitespaces matched
|
||||
if (allQuerySubstringsMatched)
|
||||
{
|
||||
var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex);
|
||||
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
|
||||
|
||||
return new MatchResult(true, SearchPrecisionScore.Regular, indexList, score);
|
||||
}
|
||||
|
||||
return new MatchResult(false, SearchPrecisionScore.Regular);
|
||||
}
|
||||
|
||||
// To get the index of the closest space which precedes the first matching index
|
||||
private static int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
||||
{
|
||||
if (spaceIndices.Count == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return spaceIndices.OrderBy(item => firstMatchIndex - item).Where(item => firstMatchIndex > item).FirstOrDefault(-1);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, string fullStringToCompareWithoutCase, string currentQuerySubstring)
|
||||
{
|
||||
var allMatch = true;
|
||||
for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++)
|
||||
{
|
||||
if (fullStringToCompareWithoutCase[startIndexToVerify + indexToCheck] !=
|
||||
currentQuerySubstring[indexToCheck])
|
||||
{
|
||||
allMatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allMatch;
|
||||
}
|
||||
|
||||
private static List<int> GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List<int> indexList)
|
||||
{
|
||||
var updatedList = new List<int>();
|
||||
|
||||
indexList.RemoveAll(x => x >= firstMatchIndexInWord);
|
||||
|
||||
updatedList.AddRange(indexList);
|
||||
|
||||
for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++)
|
||||
{
|
||||
updatedList.Add(startIndexToVerify + indexToCheck);
|
||||
}
|
||||
|
||||
return updatedList;
|
||||
}
|
||||
|
||||
private static bool AllQuerySubstringsMatched(int currentQuerySubstringIndex, int querySubstringsLength)
|
||||
{
|
||||
return currentQuerySubstringIndex >= querySubstringsLength;
|
||||
}
|
||||
|
||||
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, bool allSubstringsContainedInCompareString)
|
||||
{
|
||||
// A match found near the beginning of a string is scored more than a match found near the end
|
||||
// A match is scored more if the characters in the patterns are closer to each other,
|
||||
// while the score is lower if they are more spread out
|
||||
|
||||
// The length of the match is assigned a larger weight factor.
|
||||
const int matchLenWeightFactor = 2;
|
||||
|
||||
var score = 100 * (query.Length + 1) * matchLenWeightFactor / (1 + firstIndex + (matchLenWeightFactor * (matchLen + 1)));
|
||||
|
||||
// A match with less characters assigning more weights
|
||||
if (stringToCompare.Length - query.Length < 5)
|
||||
{
|
||||
score += 20;
|
||||
}
|
||||
else if (stringToCompare.Length - query.Length < 10)
|
||||
{
|
||||
score += 10;
|
||||
}
|
||||
|
||||
if (allSubstringsContainedInCompareString)
|
||||
{
|
||||
int count = query.Count(c => !char.IsWhiteSpace(c));
|
||||
int threshold = 4;
|
||||
if (count <= threshold)
|
||||
{
|
||||
score += count * 10;
|
||||
}
|
||||
else
|
||||
{
|
||||
score += (threshold * 10) + ((count - threshold) * 5);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CA1309 // Use ordinal string comparison (Using CurrentCultureIgnoreCase since this relates to queries input by user)
|
||||
if (string.Equals(query, stringToCompare, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
var bonusForExactMatch = 10;
|
||||
score += bonusForExactMatch;
|
||||
}
|
||||
#pragma warning restore CA1309 // Use ordinal string comparison
|
||||
|
||||
return score;
|
||||
}
|
||||
}
|
||||
24
src/common/Common.Search/GlobalSuppressions.cs
Normal file
24
src/common/Common.Search/GlobalSuppressions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.MatchResult._rawScore")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._defaultMatchOption")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._instance")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.MatchResult.#ctor(System.Boolean,Common.Search.SearchPrecisionScore)")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.MatchResult.#ctor(System.Boolean,Common.Search.SearchPrecisionScore,System.Collections.Generic.List{System.Int32},System.Int32)")]
|
||||
[assembly: SuppressMessage("Compiler", "CS8618:Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.", Justification = "Coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._instance")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._instance")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher.Separator")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.StringMatcher.#ctor")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.StringMatcher.FuzzyMatch(System.String,System.String)~Common.Search.MatchResult")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.StringMatcher.FuzzyMatch(System.String,System.String,Common.Search.MatchOption)~Common.Search.MatchResult")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1407:Arithmetic expressions should declare precedence", Justification = "migrate from stable code", Scope = "member", Target = "~M:Common.Search.StringMatcher.CalculateSearchScore(System.String,System.String,System.Int32,System.Int32,System.Boolean)~System.Int32")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "migrate from stable code", Scope = "member", Target = "~M:Common.Search.StringMatcher.FuzzyMatch(System.String,System.String,Common.Search.MatchOption,System.Int32)~Common.Search.MatchResult")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "migrate from stable code", Scope = "member", Target = "~M:Common.Search.StringMatcher.CalculateClosestSpaceIndex(System.Collections.Generic.List{System.Int32},System.Int32)~System.Int32")]
|
||||
22
src/common/Common.Search/stylecop.json
Normal file
22
src/common/Common.Search/stylecop.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings": {
|
||||
"documentationRules": {
|
||||
"companyName": "Microsoft Corporation",
|
||||
"copyrightText": "Copyright (c) {companyName}\r\nThe {companyName} licenses this file to you under the MIT license.\r\nSee the LICENSE file in the project root for more information.",
|
||||
"xmlHeader": false,
|
||||
"headerDecoration": "",
|
||||
"fileNamingConvention": "metadata",
|
||||
"documentInterfaces": false,
|
||||
"documentExposedElements": false,
|
||||
"documentInternalElements": false
|
||||
},
|
||||
"layoutRules": {
|
||||
"newlineAtEndOfFile": "require"
|
||||
},
|
||||
"orderingRules": {
|
||||
"usingDirectivesPlacement": "outsideNamespace",
|
||||
"systemUsingDirectivesFirst": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,17 +32,8 @@ namespace ManagedCommon
|
||||
/// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param>
|
||||
public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false)
|
||||
{
|
||||
string basePath;
|
||||
if (isLocalLow)
|
||||
{
|
||||
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
basePath = Constants.AppDataPath() + applicationLogPath;
|
||||
}
|
||||
|
||||
string versionedPath = Path.Combine(basePath, Version);
|
||||
string versionedPath = LogDirectoryPath(applicationLogPath, isLocalLow);
|
||||
string basePath = Path.GetDirectoryName(versionedPath);
|
||||
|
||||
if (!Directory.Exists(versionedPath))
|
||||
{
|
||||
@@ -59,6 +50,22 @@ namespace ManagedCommon
|
||||
Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath));
|
||||
}
|
||||
|
||||
public static string LogDirectoryPath(string applicationLogPath, bool isLocalLow = false)
|
||||
{
|
||||
string basePath;
|
||||
if (isLocalLow)
|
||||
{
|
||||
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
basePath = Constants.AppDataPath() + applicationLogPath;
|
||||
}
|
||||
|
||||
string versionedPath = Path.Combine(basePath, Version);
|
||||
return versionedPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes old version log folders, keeping only the current version's folder.
|
||||
/// </summary>
|
||||
|
||||
266
src/common/utils/shell_ext_registration.h
Normal file
266
src/common/utils/shell_ext_registration.h
Normal file
@@ -0,0 +1,266 @@
|
||||
// Shared runtime shell extension registration utility for PowerToys modules.
|
||||
// Provides a generic EnsureRegistered function so individual modules only need
|
||||
// to supply a specification (CLSID, sentinel, handler key paths, etc.).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
#include <shlwapi.h>
|
||||
|
||||
#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<std::wstring> dllFileCandidates; // relative filenames (pick first existing)
|
||||
std::vector<std::wstring> 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<std::wstring> extraAssociationPaths; // additional key paths (DragDropHandlers etc.) default=CLSID
|
||||
std::vector<std::wstring> systemFileAssocExtensions; // e.g. .png -> Software\\Classes\\SystemFileAssociations\\.png\\ShellEx\\ContextMenuHandlers\\<HandlerName>
|
||||
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<std::wstring>& 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<LPBYTE>(&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<const BYTE*>(&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<const BYTE*>(spec.friendlyName.c_str()), static_cast<DWORD>((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<const BYTE*>(&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<const BYTE*>(dllPath.c_str()), static_cast<DWORD>((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<const BYTE*>(tm), static_cast<DWORD>((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<LPBYTE>(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<const BYTE*>(value.c_str()), static_cast<DWORD>((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;
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,11 @@ public:
|
||||
|
||||
HRESULT result;
|
||||
|
||||
if (!RunNonElevatedEx(path.c_str(), L"", get_module_folderpath(g_hInst)))
|
||||
// Get pipe name from writer if using pipes
|
||||
std::wstring pipe_name = writer.get_pipe_name();
|
||||
std::wstring command_args = pipe_name.empty() ? L"" : pipe_name;
|
||||
|
||||
if (!RunNonElevatedEx(path.c_str(), command_args.c_str(), get_module_folderpath(g_hInst)))
|
||||
{
|
||||
result = E_FAIL;
|
||||
Trace::InvokedRet(result);
|
||||
|
||||
@@ -287,7 +287,17 @@ HRESULT ExplorerCommand::LaunchUI(CMINVOKECOMMANDINFO* pici, ipc::Writer* writer
|
||||
PROCESS_INFORMATION processInformation;
|
||||
std::wstring command_line = L"\"";
|
||||
command_line += exe_path;
|
||||
command_line += L"\"\0";
|
||||
command_line += L"\"";
|
||||
|
||||
// Add pipe name as command line argument if using pipes
|
||||
std::wstring pipe_name = writer->get_pipe_name();
|
||||
if (!pipe_name.empty())
|
||||
{
|
||||
command_line += L" ";
|
||||
command_line += pipe_name;
|
||||
}
|
||||
|
||||
command_line += L"\0";
|
||||
|
||||
CreateProcessW(
|
||||
NULL,
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<ClInclude Include="ClassFactory.h" />
|
||||
<ClInclude Include="dllmain.h" />
|
||||
<ClInclude Include="ExplorerCommand.h" />
|
||||
<ClInclude Include="RuntimeRegistration.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="resource.base.h" />
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
<ClInclude Include="dllmain.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="RuntimeRegistration.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp">
|
||||
|
||||
@@ -12,12 +12,33 @@
|
||||
#include "FileLocksmithLib/Constants.h"
|
||||
#include "FileLocksmithLib/Settings.h"
|
||||
#include "FileLocksmithLib/Trace.h"
|
||||
#include "RuntimeRegistration.h"
|
||||
|
||||
#include "dllmain.h"
|
||||
#include "Generated Files/resource.h"
|
||||
|
||||
class FileLocksmithModule : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
// Update registration based on enabled state
|
||||
void UpdateRegistration(bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
FileLocksmithRuntimeRegistration::EnsureRegistered();
|
||||
Logger::info(L"File Locksmith context menu registered");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
FileLocksmithRuntimeRegistration::Unregister();
|
||||
Logger::info(L"File Locksmith context menu unregistered");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
FileLocksmithModule()
|
||||
{
|
||||
@@ -82,7 +103,6 @@ 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);
|
||||
@@ -90,12 +110,14 @@ public:
|
||||
}
|
||||
|
||||
m_enabled = true;
|
||||
UpdateRegistration(m_enabled);
|
||||
}
|
||||
|
||||
virtual void disable() override
|
||||
{
|
||||
Logger::info(L"File Locksmith disabled");
|
||||
m_enabled = false;
|
||||
UpdateRegistration(m_enabled);
|
||||
}
|
||||
|
||||
virtual bool is_enabled() override
|
||||
@@ -128,6 +150,7 @@ private:
|
||||
{
|
||||
m_enabled = FileLocksmithSettingsInstance().GetEnabled();
|
||||
m_extended_only = FileLocksmithSettingsInstance().GetShowInExtendedContextMenu();
|
||||
UpdateRegistration(m_enabled);
|
||||
Trace::EnableFileLocksmith(m_enabled);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Header-only runtime registration for FileLocksmith context menu extension.
|
||||
#pragma once
|
||||
|
||||
#include <common/utils/shell_ext_registration.h>
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,17 @@
|
||||
#include "Constants.h"
|
||||
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <thread>
|
||||
#include <sstream>
|
||||
#include <rpc.h>
|
||||
|
||||
constexpr DWORD DefaultPipeBufferSize = 8192;
|
||||
constexpr DWORD DefaultPipeTimeoutMillis = 200;
|
||||
|
||||
namespace ipc
|
||||
{
|
||||
Writer::Writer()
|
||||
Writer::Writer() : m_pipe_handle(INVALID_HANDLE_VALUE), m_use_pipes(true)
|
||||
{
|
||||
start();
|
||||
}
|
||||
@@ -22,6 +26,32 @@ namespace ipc
|
||||
|
||||
HRESULT Writer::start()
|
||||
{
|
||||
// Try to use pipes first, fall back to file-based IPC if needed
|
||||
if (m_use_pipes)
|
||||
{
|
||||
// Generate unique pipe name similar to PowerRename
|
||||
UUID temp_uuid;
|
||||
wchar_t* uuid_chars = nullptr;
|
||||
|
||||
if (UuidCreate(&temp_uuid) == RPC_S_OK &&
|
||||
UuidToString(&temp_uuid, reinterpret_cast<RPC_WSTR*>(&uuid_chars)) == RPC_S_OK)
|
||||
{
|
||||
m_pipe_name = L"\\\\.\\pipe\\powertoys_filelocksmith_input_";
|
||||
m_pipe_name += uuid_chars;
|
||||
RpcStringFree(reinterpret_cast<RPC_WSTR*>(&uuid_chars));
|
||||
|
||||
HRESULT hr = create_pipe_server();
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
return hr;
|
||||
}
|
||||
}
|
||||
|
||||
// If pipe creation failed, fall back to file-based IPC
|
||||
m_use_pipes = false;
|
||||
}
|
||||
|
||||
// File-based IPC fallback
|
||||
std::wstring path = PTSettingsHelper::get_module_save_folder_location(constants::nonlocalizable::PowerToyName);
|
||||
path += L"\\";
|
||||
path += constants::nonlocalizable::LastRunPath;
|
||||
@@ -37,8 +67,70 @@ namespace ipc
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT Writer::create_pipe_server()
|
||||
{
|
||||
m_pipe_handle = CreateNamedPipe(
|
||||
m_pipe_name.c_str(),
|
||||
PIPE_ACCESS_DUPLEX | WRITE_DAC,
|
||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||
PIPE_UNLIMITED_INSTANCES,
|
||||
DefaultPipeBufferSize,
|
||||
DefaultPipeBufferSize,
|
||||
DefaultPipeTimeoutMillis,
|
||||
NULL);
|
||||
|
||||
if (m_pipe_handle == NULL || m_pipe_handle == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
start_pipe_server_thread();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void Writer::start_pipe_server_thread()
|
||||
{
|
||||
m_pipe_thread = std::thread([this]() {
|
||||
// This call blocks until a client process connects to the pipe
|
||||
BOOL connected = ConnectNamedPipe(m_pipe_handle, NULL);
|
||||
if (!connected)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
if (error != ERROR_PIPE_CONNECTED)
|
||||
{
|
||||
// Connection failed
|
||||
CloseHandle(m_pipe_handle);
|
||||
m_pipe_handle = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
// ERROR_PIPE_CONNECTED means client connected before ConnectNamedPipe was called
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
HRESULT Writer::add_path(LPCWSTR path)
|
||||
{
|
||||
if (m_use_pipes && m_pipe_handle != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
// Wait for pipe connection to be established
|
||||
if (m_pipe_thread.joinable())
|
||||
{
|
||||
m_pipe_thread.join();
|
||||
}
|
||||
|
||||
if (m_pipe_handle != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
// Create delimited path string
|
||||
std::wstring delimited_path = path;
|
||||
delimited_path += L"?"; // Use '?' as delimiter like PowerRename
|
||||
|
||||
DWORD path_length_bytes = static_cast<DWORD>(delimited_path.length() * sizeof(WCHAR));
|
||||
DWORD bytes_written;
|
||||
BOOL result = WriteFile(m_pipe_handle, delimited_path.c_str(), path_length_bytes, &bytes_written, NULL);
|
||||
return result ? S_OK : E_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
// File-based IPC fallback
|
||||
int length = lstrlenW(path);
|
||||
if (!m_stream.write(reinterpret_cast<const char*>(path), length * sizeof(WCHAR)))
|
||||
{
|
||||
@@ -56,7 +148,24 @@ namespace ipc
|
||||
|
||||
void Writer::finish()
|
||||
{
|
||||
add_path(L"");
|
||||
m_stream.close();
|
||||
if (m_use_pipes)
|
||||
{
|
||||
if (m_pipe_thread.joinable())
|
||||
{
|
||||
m_pipe_thread.join();
|
||||
}
|
||||
|
||||
if (m_pipe_handle != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
CloseHandle(m_pipe_handle);
|
||||
m_pipe_handle = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// File-based IPC
|
||||
add_path(L"");
|
||||
m_stream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
|
||||
namespace ipc
|
||||
{
|
||||
@@ -15,8 +17,16 @@ namespace ipc
|
||||
HRESULT add_path(LPCWSTR path);
|
||||
void finish();
|
||||
HANDLE get_read_handle();
|
||||
std::wstring get_pipe_name() const { return m_pipe_name; }
|
||||
|
||||
private:
|
||||
std::ofstream m_stream;
|
||||
HRESULT create_pipe_server();
|
||||
void start_pipe_server_thread();
|
||||
|
||||
std::ofstream m_stream; // Keep for backwards compatibility
|
||||
HANDLE m_pipe_handle;
|
||||
std::wstring m_pipe_name;
|
||||
std::thread m_pipe_thread;
|
||||
bool m_use_pipes;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "NativeMethods.h"
|
||||
#include "FileLocksmith.h"
|
||||
#include "../FileLocksmithLib/Constants.h"
|
||||
#include <sstream>
|
||||
|
||||
namespace winrt::PowerToys::FileLocksmithLib::Interop::implementation
|
||||
{
|
||||
@@ -95,6 +96,99 @@ namespace winrt::PowerToys::FileLocksmithLib::Interop::implementation
|
||||
}
|
||||
return com_array<hstring>{ result_cpp.begin(), result_cpp.end() };
|
||||
}
|
||||
|
||||
com_array<hstring> NativeMethods::ReadPathsFromPipe(hstring const& pipeName)
|
||||
{
|
||||
std::vector<std::wstring> result_cpp;
|
||||
|
||||
if (pipeName.empty())
|
||||
{
|
||||
return com_array<hstring>{ result_cpp.begin(), result_cpp.end() };
|
||||
}
|
||||
|
||||
std::wstring pipe_name_str = pipeName.c_str();
|
||||
HANDLE hPipe = INVALID_HANDLE_VALUE;
|
||||
|
||||
// Try to connect to the pipe for up to 5 seconds
|
||||
for (int attempts = 0; attempts < 50 && hPipe == INVALID_HANDLE_VALUE; attempts++)
|
||||
{
|
||||
hPipe = CreateFile(
|
||||
pipe_name_str.c_str(),
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
0,
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
NULL);
|
||||
|
||||
if (hPipe != INVALID_HANDLE_VALUE)
|
||||
break;
|
||||
|
||||
if (GetLastError() != ERROR_PIPE_BUSY)
|
||||
break;
|
||||
|
||||
if (!WaitNamedPipe(pipe_name_str.c_str(), 100))
|
||||
break;
|
||||
}
|
||||
|
||||
if (hPipe == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
// Fall back to file-based reading
|
||||
return ReadPathsFromFile();
|
||||
}
|
||||
|
||||
// Set read timeout to prevent hanging
|
||||
COMMTIMEOUTS timeouts = { 0 };
|
||||
timeouts.ReadIntervalTimeout = 100;
|
||||
timeouts.ReadTotalTimeoutConstant = 3000; // 3 second timeout
|
||||
timeouts.ReadTotalTimeoutMultiplier = 0;
|
||||
SetCommTimeouts(hPipe, &timeouts);
|
||||
|
||||
// Read from pipe - collect all data first
|
||||
const DWORD BUFSIZE = 4096;
|
||||
std::wstring allData;
|
||||
WCHAR chBuf[BUFSIZE];
|
||||
DWORD dwRead;
|
||||
BOOL bSuccess;
|
||||
|
||||
// Read with timeout protection
|
||||
int read_attempts = 0;
|
||||
const int max_read_attempts = 30; // Maximum 30 read attempts
|
||||
|
||||
for (;;)
|
||||
{
|
||||
bSuccess = ReadFile(hPipe, chBuf, BUFSIZE * sizeof(WCHAR), &dwRead, NULL);
|
||||
|
||||
if (!bSuccess || dwRead == 0 || ++read_attempts > max_read_attempts)
|
||||
break;
|
||||
|
||||
// Append to accumulated data
|
||||
allData.append(chBuf, dwRead / sizeof(WCHAR));
|
||||
|
||||
if (!bSuccess)
|
||||
break;
|
||||
}
|
||||
|
||||
CloseHandle(hPipe);
|
||||
|
||||
// Parse all data using delimiter
|
||||
if (!allData.empty())
|
||||
{
|
||||
std::wstringstream ss(allData);
|
||||
std::wstring item;
|
||||
wchar_t delimiter = L'?';
|
||||
|
||||
while (std::getline(ss, item, delimiter))
|
||||
{
|
||||
if (!item.empty())
|
||||
{
|
||||
result_cpp.push_back(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return com_array<hstring>{ result_cpp.begin(), result_cpp.end() };
|
||||
}
|
||||
|
||||
bool NativeMethods::StartAsElevated(array_view<hstring const> paths)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace winrt::PowerToys::FileLocksmithLib::Interop::implementation
|
||||
static com_array<winrt::PowerToys::FileLocksmithLib::Interop::ProcessResult> FindProcessesRecursive(array_view<hstring const> paths);
|
||||
static hstring PidToFullPath(uint32_t pid);
|
||||
static com_array<hstring> ReadPathsFromFile();
|
||||
static com_array<hstring> ReadPathsFromPipe(hstring const& pipeName);
|
||||
static bool StartAsElevated(array_view<hstring const> paths);
|
||||
static bool SetDebugPrivilege();
|
||||
static bool IsProcessElevated();
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace FileLocksmithUI
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
public static string[] CommandLineArgs { get; private set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="App"/> class.
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
@@ -43,6 +45,10 @@ namespace FileLocksmithUI
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
// Store command line arguments for access by other classes
|
||||
string[] commandArgs = Environment.GetCommandLineArgs();
|
||||
CommandLineArgs = commandArgs.Length > 1 ? commandArgs.Skip(1).ToArray() : Array.Empty<string>();
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredFileLocksmithEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
|
||||
@@ -79,8 +79,47 @@ namespace PowerToys.FileLocksmithUI.ViewModels
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
paths = NativeMethods.ReadPathsFromFile();
|
||||
Logger.LogInfo($"Starting FileLocksmith with {paths.Length} files selected.");
|
||||
// Check if pipe name was passed as command line argument
|
||||
string[] args = FileLocksmithUI.App.CommandLineArgs;
|
||||
string pipeName = null;
|
||||
|
||||
// Look for pipe name in command line arguments
|
||||
if (args != null && args.Length > 0)
|
||||
{
|
||||
foreach (string arg in args)
|
||||
{
|
||||
if (arg.StartsWith(@"\\.\pipe\"))
|
||||
{
|
||||
pipeName = arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to read from pipe first, fall back to file if needed
|
||||
if (!string.IsNullOrEmpty(pipeName))
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"Attempting to read from pipe: {pipeName}");
|
||||
paths = NativeMethods.ReadPathsFromPipe(pipeName);
|
||||
Logger.LogInfo($"Successfully read {paths.Length} files from pipe.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to read from pipe {pipeName}: {ex.Message}");
|
||||
Logger.LogInfo("Falling back to file-based IPC.");
|
||||
paths = NativeMethods.ReadPathsFromFile();
|
||||
Logger.LogInfo($"Fallback: Read {paths.Length} files from file.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("No pipe name provided, using file-based IPC.");
|
||||
paths = NativeMethods.ReadPathsFromFile();
|
||||
Logger.LogInfo($"Read {paths.Length} files from file.");
|
||||
}
|
||||
|
||||
LoadProcessesCommand = new AsyncRelayCommand(LoadProcessesAsync);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MSLLHOOKSTRUCT*>(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.");
|
||||
|
||||
@@ -31,3 +31,7 @@ void InclusiveCrosshairsDisable();
|
||||
bool InclusiveCrosshairsIsEnabled();
|
||||
void InclusiveCrosshairsSwitch();
|
||||
void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings);
|
||||
void InclusiveCrosshairsRequestUpdatePosition();
|
||||
void InclusiveCrosshairsEnsureOn();
|
||||
void InclusiveCrosshairsEnsureOff();
|
||||
void InclusiveCrosshairsSetExternalControl(bool enabled);
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
#include "trace.h"
|
||||
#include "InclusiveCrosshairs.h"
|
||||
#include "common/utils/color.h"
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
extern void InclusiveCrosshairsRequestUpdatePosition();
|
||||
extern void InclusiveCrosshairsEnsureOn();
|
||||
extern void InclusiveCrosshairsEnsureOff();
|
||||
extern void InclusiveCrosshairsSetExternalControl(bool enabled);
|
||||
|
||||
// Non-Localizable strings
|
||||
namespace
|
||||
@@ -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<bool> stopX{ false };
|
||||
std::atomic<bool> stopY{ false };
|
||||
|
||||
// positions and speeds
|
||||
int currentXPos{ 0 };
|
||||
int currentYPos{ 0 };
|
||||
int currentXSpeed{ 0 }; // pixels per base window
|
||||
int currentYSpeed{ 0 }; // pixels per base window
|
||||
int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan
|
||||
|
||||
// Fractional accumulators to spread movement across 10ms ticks
|
||||
double xFraction{ 0.0 };
|
||||
double yFraction{ 0.0 };
|
||||
|
||||
// Speeds represent pixels per 200ms (min 5, max 60 enforced by UI/settings)
|
||||
int fastHSpeed{ 30 }; // pixels per base window
|
||||
int slowHSpeed{ 5 }; // pixels per base window
|
||||
int fastVSpeed{ 30 }; // pixels per base window
|
||||
int slowVSpeed{ 5 }; // pixels per base window
|
||||
};
|
||||
|
||||
std::shared_ptr<State> m_state;
|
||||
|
||||
// Worker threads
|
||||
std::thread m_xThread;
|
||||
std::thread m_yThread;
|
||||
|
||||
// Gliding cursor state machine
|
||||
std::atomic<int> m_glideState{ 0 }; // 0..4 like the AHK script
|
||||
|
||||
// Timer configuration: 10ms tick, speeds are defined per 200ms base window
|
||||
static constexpr int kTimerTickMs = 10;
|
||||
static constexpr int kBaseSpeedTickMs = 200; // mapping period for configured pixel counts
|
||||
|
||||
// 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<State>();
|
||||
init_settings();
|
||||
};
|
||||
|
||||
// Destroy the powertoy and free memory
|
||||
virtual void destroy() override
|
||||
{
|
||||
StopXTimer();
|
||||
StopYTimer();
|
||||
// Release shared state so worker threads (if any) exit when weak_ptr lock fails
|
||||
m_state.reset();
|
||||
delete this;
|
||||
}
|
||||
|
||||
@@ -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<HotkeyEx> 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<State>& s)
|
||||
{
|
||||
int screenW = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
s->currentYPos = screenH / 2;
|
||||
|
||||
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
|
||||
const double perTick = (static_cast<double>(s->currentXSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
|
||||
s->xFraction += perTick;
|
||||
int step = static_cast<int>(s->xFraction);
|
||||
if (step > 0)
|
||||
{
|
||||
s->xFraction -= step;
|
||||
s->currentXPos += step;
|
||||
}
|
||||
|
||||
s->xPosSnapshot = s->currentXPos;
|
||||
if (s->currentXPos >= screenW)
|
||||
{
|
||||
s->currentXPos = 0;
|
||||
s->currentXSpeed = s->fastHSpeed;
|
||||
s->xPosSnapshot = 0;
|
||||
s->xFraction = 0.0; // reset fractional remainder on wrap
|
||||
}
|
||||
SetCursorPos(s->currentXPos, s->currentYPos);
|
||||
// Ensure overlay crosshairs follow immediately
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
}
|
||||
|
||||
static void PositionCursorY(const std::shared_ptr<State>& s)
|
||||
{
|
||||
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
// Keep X at snapshot
|
||||
// Use s->xPosSnapshot captured during X pass
|
||||
|
||||
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
|
||||
const double perTick = (static_cast<double>(s->currentYSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
|
||||
s->yFraction += perTick;
|
||||
int step = static_cast<int>(s->yFraction);
|
||||
if (step > 0)
|
||||
{
|
||||
s->yFraction -= step;
|
||||
s->currentYPos += step;
|
||||
}
|
||||
|
||||
if (s->currentYPos >= screenH)
|
||||
{
|
||||
s->currentYPos = 0;
|
||||
s->currentYSpeed = s->fastVSpeed;
|
||||
s->yFraction = 0.0; // reset fractional remainder on wrap
|
||||
}
|
||||
SetCursorPos(s->xPosSnapshot, s->currentYPos);
|
||||
// Ensure overlay crosshairs follow immediately
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
}
|
||||
|
||||
void StartXTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s->stopX = false;
|
||||
std::weak_ptr<State> wp = s;
|
||||
m_xThread = std::thread([wp]() {
|
||||
while (true)
|
||||
{
|
||||
auto sp = wp.lock();
|
||||
if (!sp || sp->stopX.load())
|
||||
{
|
||||
break;
|
||||
}
|
||||
PositionCursorX(sp);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void StopXTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (s)
|
||||
{
|
||||
s->stopX = true;
|
||||
}
|
||||
if (m_xThread.joinable())
|
||||
{
|
||||
m_xThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void StartYTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s->stopY = false;
|
||||
std::weak_ptr<State> wp = s;
|
||||
m_yThread = std::thread([wp]() {
|
||||
while (true)
|
||||
{
|
||||
auto sp = wp.lock();
|
||||
if (!sp || sp->stopY.load())
|
||||
{
|
||||
break;
|
||||
}
|
||||
PositionCursorY(sp);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void StopYTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (s)
|
||||
{
|
||||
s->stopY = true;
|
||||
}
|
||||
if (m_yThread.joinable())
|
||||
{
|
||||
m_yThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void HandleGlidingHotkey()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Simulate the AHK state machine
|
||||
int state = m_glideState.load();
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
// Ensure crosshairs on (do not toggle off if already on)
|
||||
InclusiveCrosshairsEnsureOn();
|
||||
// Disable internal mouse hook so we control position updates explicitly
|
||||
InclusiveCrosshairsSetExternalControl(true);
|
||||
|
||||
s->currentXPos = 0;
|
||||
s->currentXSpeed = s->fastHSpeed;
|
||||
s->xFraction = 0.0;
|
||||
s->yFraction = 0.0;
|
||||
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
|
||||
SetCursorPos(0, y);
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
m_glideState = 1;
|
||||
StartXTimer();
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
// Slow horizontal
|
||||
s->currentXSpeed = s->slowHSpeed;
|
||||
m_glideState = 2;
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
// Stop horizontal, start vertical (fast)
|
||||
StopXTimer();
|
||||
s->currentYSpeed = s->fastVSpeed;
|
||||
s->currentYPos = 0;
|
||||
s->yFraction = 0.0;
|
||||
SetCursorPos(s->xPosSnapshot, s->currentYPos);
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
m_glideState = 3;
|
||||
StartYTimer();
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
// Slow vertical
|
||||
s->currentYSpeed = s->slowVSpeed;
|
||||
m_glideState = 4;
|
||||
break;
|
||||
}
|
||||
case 4:
|
||||
default:
|
||||
{
|
||||
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
|
||||
StopYTimer();
|
||||
m_glideState = 0;
|
||||
LeftClick();
|
||||
InclusiveCrosshairsEnsureOff();
|
||||
InclusiveCrosshairsSetExternalControl(false);
|
||||
s->xFraction = 0.0;
|
||||
s->yFraction = 0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the settings file.
|
||||
void init_settings()
|
||||
{
|
||||
@@ -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<unsigned char>(hotkey.get_code());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse Gliding Cursor HotKey
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT);
|
||||
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
|
||||
m_glidingHotkey.win = hotkey.win_pressed();
|
||||
m_glidingHotkey.ctrl = hotkey.ctrl_pressed();
|
||||
m_glidingHotkey.shift = hotkey.shift_pressed();
|
||||
m_glidingHotkey.alt = hotkey.alt_pressed();
|
||||
m_glidingHotkey.key = static_cast<unsigned char>(hotkey.get_code());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// note that this is also defined in src\settings-ui\Settings.UI.Library\MousePointerCrosshairsProperties.cs, DefaultGlidingCursorActivationShortcut
|
||||
// both need to be kept in sync!
|
||||
Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+.");
|
||||
m_glidingHotkey.win = true;
|
||||
m_glidingHotkey.alt = true;
|
||||
m_glidingHotkey.ctrl = false;
|
||||
m_glidingHotkey.shift = false;
|
||||
m_glidingHotkey.key = VK_OEM_PERIOD;
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse Opacity
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY);
|
||||
@@ -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 <int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
int value = static_cast<int>(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<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 5 && value <= 60)
|
||||
{
|
||||
m_state->fastHSpeed = value;
|
||||
m_state->fastVSpeed = value;
|
||||
}
|
||||
else if (value < 5)
|
||||
{
|
||||
m_state->fastHSpeed = 5; m_state->fastVSpeed = 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_state->fastHSpeed = 60; m_state->fastVSpeed = 60;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize gliding travel speed from settings. Using default 25.");
|
||||
if (m_state)
|
||||
{
|
||||
m_state->fastHSpeed = 25;
|
||||
m_state->fastVSpeed = 25;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse Delay speed (slow speed mapping)
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_DELAY_SPEED);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 5 && value <= 60)
|
||||
{
|
||||
m_state->slowHSpeed = value;
|
||||
m_state->slowVSpeed = value;
|
||||
}
|
||||
else if (value < 5)
|
||||
{
|
||||
m_state->slowHSpeed = 5; m_state->slowVSpeed = 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_state->slowHSpeed = 60; m_state->slowVSpeed = 60;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize gliding delay speed from settings. Using default 5.");
|
||||
if (m_state)
|
||||
{
|
||||
m_state->slowHSpeed = 5;
|
||||
m_state->slowVSpeed = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info("Mouse Pointer Crosshairs settings are empty");
|
||||
}
|
||||
if (!m_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()
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -1560,5 +1561,98 @@ namespace MouseWithoutBorders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DisableEasyMouseWhenForegroundWindowIsFullscreenSetting()
|
||||
{
|
||||
return Setting.Values.DisableEasyMouseWhenForegroundWindowIsFullscreen;
|
||||
}
|
||||
|
||||
private static bool IsAppIgnoredByEasyMouseFullscreenCheck(IntPtr foregroundWindowHandle)
|
||||
{
|
||||
if (NativeMethods.GetWindowThreadProcessId(foregroundWindowHandle, out var processId) == 0)
|
||||
{
|
||||
Logger.LogDebug($"GetWindowThreadProcessId failed with error : {Marshal.GetLastWin32Error()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var processHandle = NativeMethods.OpenProcess(0x1000, false, processId);
|
||||
if (processHandle == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint maxPath = 260;
|
||||
var nameBuffer = new char[maxPath];
|
||||
if (!NativeMethods.QueryFullProcessImageName(
|
||||
processHandle, NativeMethods.QUERY_FULL_PROCESS_NAME_FLAGS.DEFAULT, nameBuffer, ref maxPath))
|
||||
{
|
||||
Logger.LogDebug($"QueryFullProcessImageName failed with error : {Marshal.GetLastWin32Error()}");
|
||||
NativeMethods.CloseHandle(processHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
NativeMethods.CloseHandle(processHandle);
|
||||
|
||||
var name = new string(nameBuffer, 0, (int)maxPath);
|
||||
|
||||
var excludedApps = Setting.Values.EasyMouseFullscreenSwitchBlockExcludedApps;
|
||||
|
||||
return excludedApps.Contains(Path.GetFileNameWithoutExtension(name), StringComparer.OrdinalIgnoreCase)
|
||||
|| excludedApps.Contains(Path.GetFileName(name), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static bool IsEasyMouseBlockedByFullscreenWindow()
|
||||
{
|
||||
var shellHandle = NativeMethods.GetShellWindow();
|
||||
var desktopHandle = NativeMethods.GetDesktopWindow();
|
||||
var foregroundHandle = NativeMethods.GetForegroundWindow();
|
||||
|
||||
// If the foreground window is either the desktop or the Windows shell, we are not in fullscreen mode.
|
||||
if (foregroundHandle.Equals(shellHandle) || foregroundHandle.Equals(desktopHandle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NativeMethods.SHQueryUserNotificationState(out var userNotificationState) != 0)
|
||||
{
|
||||
Logger.LogDebug($"SHQueryUserNotificationState failed with error : {Marshal.GetLastWin32Error()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (userNotificationState)
|
||||
{
|
||||
// An application running in full screen mode, check if the foreground window is
|
||||
// listed as ignored in the settings.
|
||||
case NativeMethods.USER_NOTIFICATION_STATE.BUSY:
|
||||
case NativeMethods.USER_NOTIFICATION_STATE.RUNNING_D3D_FULL_SCREEN:
|
||||
case NativeMethods.USER_NOTIFICATION_STATE.PRESENTATION_MODE:
|
||||
return !IsAppIgnoredByEasyMouseFullscreenCheck(foregroundHandle);
|
||||
|
||||
// No full screen app running.
|
||||
case NativeMethods.USER_NOTIFICATION_STATE.NOT_PRESENT:
|
||||
case NativeMethods.USER_NOTIFICATION_STATE.ACCEPTS_NOTIFICATIONS:
|
||||
case NativeMethods.USER_NOTIFICATION_STATE.QUIET_TIME:
|
||||
// Cannot determine
|
||||
case NativeMethods.USER_NOTIFICATION_STATE.APP:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a machine switch triggered by EasyMouse would be allowed to proceed due to other settings.
|
||||
/// </summary>
|
||||
/// <returns>A boolean that tells us if the switch isn't blocked by any other settings</returns>
|
||||
internal static bool IsEasyMouseSwitchAllowed()
|
||||
{
|
||||
// Never prevent a switch if we are not moving out of the host machine.
|
||||
if (!DisableEasyMouseWhenForegroundWindowIsFullscreenSetting() || DesMachineID != MachineID)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the switch is blocked by a full-screen window running in the foreground
|
||||
return !IsEasyMouseBlockedByFullscreenWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,16 @@ namespace MouseWithoutBorders.Class
|
||||
[DllImport("user32.dll", SetLastError = false)]
|
||||
internal static extern IntPtr GetDesktopWindow();
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial IntPtr GetShellWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetWindowDC(IntPtr hWnd);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern int DrawText(IntPtr hDC, string lpString, int nCount, ref RECT lpRect, uint uFormat);
|
||||
|
||||
@@ -291,6 +298,17 @@ namespace MouseWithoutBorders.Class
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[LibraryImport("kernel32.dll",
|
||||
EntryPoint = "QueryFullProcessImageNameW",
|
||||
SetLastError = true,
|
||||
StringMarshalling = StringMarshalling.Utf16)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool QueryFullProcessImageName(
|
||||
IntPtr hProcess, QUERY_FULL_PROCESS_NAME_FLAGS dwFlags, [Out] char[] lpExeName, ref uint lpdwSize);
|
||||
|
||||
[LibraryImport("shell32.dll", SetLastError = true)]
|
||||
internal static partial int SHQueryUserNotificationState(out USER_NOTIFICATION_STATE state);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct POINT
|
||||
{
|
||||
@@ -333,11 +351,11 @@ namespace MouseWithoutBorders.Class
|
||||
|
||||
[DllImport("ntdll.dll")]
|
||||
internal static extern int NtQueryInformationProcess(
|
||||
IntPtr hProcess,
|
||||
int processInformationClass /* 0 */,
|
||||
ref PROCESS_BASIC_INFORMATION processBasicInformation,
|
||||
uint processInformationLength,
|
||||
out uint returnLength);
|
||||
IntPtr hProcess,
|
||||
int processInformationClass /* 0 */,
|
||||
ref PROCESS_BASIC_INFORMATION processBasicInformation,
|
||||
uint processInformationLength,
|
||||
out uint returnLength);
|
||||
#endif
|
||||
|
||||
#if USE_GetSecurityDescriptorSacl
|
||||
@@ -632,14 +650,14 @@ namespace MouseWithoutBorders.Class
|
||||
{
|
||||
internal int LowPart;
|
||||
internal int HighPart;
|
||||
}// end struct
|
||||
} // end struct
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct LUID_AND_ATTRIBUTES
|
||||
{
|
||||
internal LUID Luid;
|
||||
internal int Attributes;
|
||||
}// end struct
|
||||
} // end struct
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct TOKEN_PRIVILEGES
|
||||
@@ -670,23 +688,23 @@ namespace MouseWithoutBorders.Class
|
||||
internal const int TOKEN_ADJUST_SESSIONID = 0x0100;
|
||||
|
||||
internal const int TOKEN_ALL_ACCESS_P = STANDARD_RIGHTS_REQUIRED |
|
||||
TOKEN_ASSIGN_PRIMARY |
|
||||
TOKEN_DUPLICATE |
|
||||
TOKEN_IMPERSONATE |
|
||||
TOKEN_QUERY |
|
||||
TOKEN_QUERY_SOURCE |
|
||||
TOKEN_ADJUST_PRIVILEGES |
|
||||
TOKEN_ADJUST_GROUPS |
|
||||
TOKEN_ADJUST_DEFAULT;
|
||||
TOKEN_ASSIGN_PRIMARY |
|
||||
TOKEN_DUPLICATE |
|
||||
TOKEN_IMPERSONATE |
|
||||
TOKEN_QUERY |
|
||||
TOKEN_QUERY_SOURCE |
|
||||
TOKEN_ADJUST_PRIVILEGES |
|
||||
TOKEN_ADJUST_GROUPS |
|
||||
TOKEN_ADJUST_DEFAULT;
|
||||
|
||||
internal const int TOKEN_ALL_ACCESS = TOKEN_ALL_ACCESS_P | TOKEN_ADJUST_SESSIONID;
|
||||
|
||||
internal const int TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY;
|
||||
|
||||
internal const int TOKEN_WRITE = STANDARD_RIGHTS_WRITE |
|
||||
TOKEN_ADJUST_PRIVILEGES |
|
||||
TOKEN_ADJUST_GROUPS |
|
||||
TOKEN_ADJUST_DEFAULT;
|
||||
TOKEN_ADJUST_PRIVILEGES |
|
||||
TOKEN_ADJUST_GROUPS |
|
||||
TOKEN_ADJUST_DEFAULT;
|
||||
|
||||
internal const int TOKEN_EXECUTE = STANDARD_RIGHTS_EXECUTE;
|
||||
|
||||
@@ -940,6 +958,30 @@ namespace MouseWithoutBorders.Class
|
||||
NameDnsDomain = 12,
|
||||
}
|
||||
|
||||
internal enum MONITOR_FROM_WINDOW_FLAGS : uint
|
||||
{
|
||||
DEFAULT_TO_NULL = 0x00000000,
|
||||
DEFAULT_TO_PRIMARY = 0x00000001,
|
||||
DEFAULT_TO_NEAREST = 0x00000002,
|
||||
}
|
||||
|
||||
internal enum QUERY_FULL_PROCESS_NAME_FLAGS : uint
|
||||
{
|
||||
DEFAULT = 0x00000000,
|
||||
PROCESS_NAME_NATIVE = 0x00000001,
|
||||
}
|
||||
|
||||
internal enum USER_NOTIFICATION_STATE
|
||||
{
|
||||
NOT_PRESENT = 1,
|
||||
BUSY = 2,
|
||||
RUNNING_D3D_FULL_SCREEN = 3,
|
||||
PRESENTATION_MODE = 4,
|
||||
ACCEPTS_NOTIFICATIONS = 5,
|
||||
QUIET_TIME = 6,
|
||||
APP = 7,
|
||||
}
|
||||
|
||||
[DllImport("secur32.dll", CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
internal static extern bool GetUserNameEx(int nameFormat, StringBuilder userName, ref uint userNameSize);
|
||||
|
||||
@@ -414,6 +414,44 @@ namespace MouseWithoutBorders.Class
|
||||
}
|
||||
}
|
||||
|
||||
internal bool DisableEasyMouseWhenForegroundWindowIsFullscreen
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
return _properties.DisableEasyMouseWhenForegroundWindowIsFullscreen;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
_properties.DisableEasyMouseWhenForegroundWindowIsFullscreen = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal HashSet<string> EasyMouseFullscreenSwitchBlockExcludedApps
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
return _properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
_properties.EasyMouseFullscreenSwitchBlockExcludedApps.Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal string Enc(string st, bool dec, DataProtectionScope protectionScope)
|
||||
{
|
||||
if (st == null || st.Length < 1)
|
||||
|
||||
@@ -66,13 +66,17 @@ internal static class Event
|
||||
try
|
||||
{
|
||||
Common.PaintCount = 0;
|
||||
bool switchByMouseEnabled = IsSwitchingByMouseEnabled();
|
||||
|
||||
if (switchByMouseEnabled && Common.Sk != null && (Common.DesMachineID == Common.MachineID || !Setting.Values.MoveMouseRelatively) && e.dwFlags == Common.WM_MOUSEMOVE)
|
||||
// Check if easy mouse setting is enabled.
|
||||
bool isEasyMouseEnabled = IsSwitchingByMouseEnabled();
|
||||
|
||||
if (isEasyMouseEnabled && Common.Sk != null && (Common.DesMachineID == Common.MachineID || !Setting.Values.MoveMouseRelatively) && e.dwFlags == Common.WM_MOUSEMOVE)
|
||||
{
|
||||
Point p = MachineStuff.MoveToMyNeighbourIfNeeded(e.X, e.Y, MachineStuff.desMachineID);
|
||||
|
||||
if (!p.IsEmpty)
|
||||
// Check if easy mouse switches are disabled when an application is running in fullscreen mode,
|
||||
// if they are, check that there is no application running in fullscreen mode before switching.
|
||||
if (!p.IsEmpty && Common.IsEasyMouseSwitchAllowed())
|
||||
{
|
||||
Common.HasSwitchedMachineSinceLastCopy = true;
|
||||
|
||||
@@ -165,7 +169,8 @@ internal static class Event
|
||||
string newDesMachineName = MachineStuff.NameFromID(newDesMachineID);
|
||||
|
||||
if (!Common.IsConnectedTo(newDesMachineID))
|
||||
{// Connection lost, cancel switching
|
||||
{
|
||||
// Connection lost, cancel switching
|
||||
Logger.LogDebug("No active connection found for " + newDesMachineName);
|
||||
|
||||
// ShowToolTip("No active connection found for [" + newDesMachineName + "]!", 500);
|
||||
|
||||
@@ -123,6 +123,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
|
||||
<ClInclude Include="settings.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="new_utilities.h" />
|
||||
<ClInclude Include="RuntimeRegistration.h" />
|
||||
<ClInclude Include="resource.base.h" />
|
||||
<ClInclude Include="template_folder.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
<ClInclude Include="helpers_variables.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="RuntimeRegistration.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Header-only runtime registration for New+ Win10 context menu.
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <common/utils/shell_ext_registration.h>
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,31 @@
|
||||
#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
|
||||
{
|
||||
private:
|
||||
// Update registration based on enabled state
|
||||
void UpdateRegistration(bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
|
||||
Logger::info(L"New+ context menu registered");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
NewPlusRuntimeRegistration::Unregister();
|
||||
Logger::info(L"New+ context menu unregistered");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
NewModule()
|
||||
{
|
||||
@@ -93,10 +114,13 @@ public:
|
||||
|
||||
// Log telemetry
|
||||
Trace::EventToggleOnOff(true);
|
||||
|
||||
newplus::utilities::register_msix_package();
|
||||
if (package::IsWin11OrGreater())
|
||||
{
|
||||
newplus::utilities::register_msix_package();
|
||||
}
|
||||
|
||||
powertoy_new_enabled = true;
|
||||
UpdateRegistration(powertoy_new_enabled);
|
||||
}
|
||||
|
||||
virtual void disable() override
|
||||
@@ -142,11 +166,13 @@ private:
|
||||
Trace::EventToggleOnOff(false);
|
||||
}
|
||||
powertoy_new_enabled = false;
|
||||
UpdateRegistration(powertoy_new_enabled);
|
||||
}
|
||||
|
||||
void init_settings()
|
||||
{
|
||||
powertoy_new_enabled = NewSettingsInstance().GetEnabled();
|
||||
UpdateRegistration(powertoy_new_enabled);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -246,9 +246,17 @@ LRESULT WindowBorder::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexce
|
||||
case WM_ERASEBKGND:
|
||||
return TRUE;
|
||||
|
||||
// prevent from beeping if the border was clicked
|
||||
// Prevent from beeping if the border was clicked
|
||||
case WM_SETCURSOR:
|
||||
{
|
||||
HCURSOR hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
if (hCursor)
|
||||
{
|
||||
SetCursor(hCursor);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
|
||||
@@ -49,7 +49,9 @@ namespace Awake.Core
|
||||
|
||||
private static DateTimeOffset ExpireAt { get; set; }
|
||||
|
||||
private static readonly CompositeFormat AwakeMinute = CompositeFormat.Parse(Resources.AWAKE_MINUTE);
|
||||
private static readonly CompositeFormat AwakeMinutes = CompositeFormat.Parse(Resources.AWAKE_MINUTES);
|
||||
private static readonly CompositeFormat AwakeHour = CompositeFormat.Parse(Resources.AWAKE_HOUR);
|
||||
private static readonly CompositeFormat AwakeHours = CompositeFormat.Parse(Resources.AWAKE_HOURS);
|
||||
private static readonly BlockingCollection<ExecutionState> _stateQueue;
|
||||
private static CancellationTokenSource _tokenSource;
|
||||
@@ -451,7 +453,7 @@ namespace Awake.Core
|
||||
Dictionary<string, uint> optionsList = new()
|
||||
{
|
||||
{ string.Format(CultureInfo.InvariantCulture, AwakeMinutes, 30), 1800 },
|
||||
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 1), 3600 },
|
||||
{ string.Format(CultureInfo.InvariantCulture, AwakeHour, 1), 3600 },
|
||||
{ string.Format(CultureInfo.InvariantCulture, AwakeHours, 2), 7200 },
|
||||
};
|
||||
return optionsList;
|
||||
|
||||
@@ -159,6 +159,15 @@ namespace Awake.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} hour.
|
||||
/// </summary>
|
||||
internal static string AWAKE_HOUR {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_HOUR", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} hours.
|
||||
/// </summary>
|
||||
@@ -240,6 +249,15 @@ namespace Awake.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} minute.
|
||||
/// </summary>
|
||||
internal static string AWAKE_MINUTE {
|
||||
get {
|
||||
return ResourceManager.GetString("AWAKE_MINUTE", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} minutes.
|
||||
/// </summary>
|
||||
|
||||
@@ -123,6 +123,10 @@
|
||||
<data name="AWAKE_EXIT" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
</data>
|
||||
<data name="AWAKE_HOUR" xml:space="preserve">
|
||||
<value>{0} hour</value>
|
||||
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_HOURS" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
|
||||
@@ -142,6 +146,10 @@
|
||||
<value>Keep awake until expiration date and time</value>
|
||||
<comment>Keep the system awake until expiration date and time</comment>
|
||||
</data>
|
||||
<data name="AWAKE_MINUTE" xml:space="preserve">
|
||||
<value>{0} minute</value>
|
||||
<comment>{0} shouldn't be removed. It will be replaced by the number 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
|
||||
</data>
|
||||
<data name="AWAKE_MINUTES" xml:space="preserve">
|
||||
<value>{0} minutes</value>
|
||||
<comment>{0} shouldn't be removed. It will be replaced by a number greater than 1 at runtime by the application. Used for defining a period to keep the PC awake.</comment>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -181,15 +182,15 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
if (more is not null)
|
||||
{
|
||||
MoreCommands = more
|
||||
.Select(item =>
|
||||
.Select<IContextItem, IContextItemViewModel>(item =>
|
||||
{
|
||||
if (item is ICommandContextItem contextItem)
|
||||
{
|
||||
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||
return new CommandContextItemViewModel(contextItem, PageContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||
return new SeparatorViewModel();
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
@@ -237,8 +238,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
FastInitializeProperties();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("error fast initializing CommandItemViewModel", ex);
|
||||
Command = new(null, PageContext);
|
||||
_itemTitle = "Error";
|
||||
Subtitle = "Item failed to load";
|
||||
@@ -257,9 +259,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
SlowInitializeProperties();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Initialized |= InitializedState.Error;
|
||||
Logger.LogError("error slow initializing CommandItemViewModel", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -272,8 +275,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
InitializeProperties();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("error initializing CommandItemViewModel", ex);
|
||||
Command = new(null, PageContext);
|
||||
_itemTitle = "Error";
|
||||
Subtitle = "Item failed to load";
|
||||
@@ -342,15 +346,15 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
if (more is not null)
|
||||
{
|
||||
var newContextMenu = more
|
||||
.Select(item =>
|
||||
.Select<IContextItem, IContextItemViewModel>(item =>
|
||||
{
|
||||
if (item is ICommandContextItem contextItem)
|
||||
{
|
||||
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||
return new CommandContextItemViewModel(contextItem, PageContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||
return new SeparatorViewModel();
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -29,6 +29,15 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
|
||||
public IconInfoViewModel Icon { get; private set; }
|
||||
|
||||
// UNDER NO CIRCUMSTANCES MAY SOMEONE WRITE TO THIS DICTIONARY.
|
||||
// This is our copy of the data from the extension.
|
||||
// Adding values to it does not add to the extension.
|
||||
// Modifying it will not modify the extension
|
||||
// (except it might, if the dictionary was passed by ref)
|
||||
private Dictionary<string, ExtensionObject<object>>? _properties;
|
||||
|
||||
public IReadOnlyDictionary<string, ExtensionObject<object>>? Properties => _properties?.AsReadOnly();
|
||||
|
||||
public CommandViewModel(ICommand? command, WeakReference<IPageContext> pageContext)
|
||||
: base(pageContext)
|
||||
{
|
||||
@@ -80,6 +89,11 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
UpdateProperty(nameof(Icon));
|
||||
}
|
||||
|
||||
if (model is IExtendedAttributesProvider command2)
|
||||
{
|
||||
UpdatePropertiesFromExtension(command2);
|
||||
}
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
@@ -130,4 +144,26 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePropertiesFromExtension(IExtendedAttributesProvider? model)
|
||||
{
|
||||
var propertiesFromExtension = model?.GetProperties();
|
||||
if (propertiesFromExtension == null)
|
||||
{
|
||||
_properties = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_properties = [];
|
||||
|
||||
// COPY the properties into us.
|
||||
// The IDictionary that was passed to us may be marshalled by-ref or by-value, we _don't know_.
|
||||
//
|
||||
// If it's by-ref, the values are arbitrary objects that are out-of-proc.
|
||||
// If it's bu-value, then everything is in-proc, and we can't mutate the data.
|
||||
foreach (var property in propertiesFromExtension)
|
||||
{
|
||||
_properties.Add(property.Key, new(property.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<IFilter> _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<IPageContext> 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));
|
||||
}
|
||||
}
|
||||
@@ -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<IFilters> _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<IFilters> filters, WeakReference<IPageContext> context)
|
||||
: base(context)
|
||||
{
|
||||
_filtersModel = filters;
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_filtersModel.Unsafe is not null)
|
||||
{
|
||||
var filters = _filtersModel.Unsafe.GetFilters();
|
||||
Filters = filters.Select<IFilterItem, IFilterItemViewModel>(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 = [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -27,6 +27,8 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
|
||||
IRandomAccessStreamReference? IIconData.Data => Data.Unsafe;
|
||||
|
||||
public string? FontFamily { get; private set; }
|
||||
|
||||
public IconDataViewModel(IIconData? icon)
|
||||
{
|
||||
_model = new(icon);
|
||||
@@ -43,5 +45,22 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
|
||||
Icon = model.Icon;
|
||||
Data = new(model.Data);
|
||||
|
||||
if (model is IExtendedAttributesProvider icon2)
|
||||
{
|
||||
var props = icon2.GetProperties();
|
||||
|
||||
// From Raymond Chen:
|
||||
// Make sure you don't try do do something like
|
||||
// icon2.GetProperties().TryGetValue("awesomeKey", out var awesomeValue);
|
||||
// icon2.GetProperties().TryGetValue("slackerKey", out var slackerValue);
|
||||
// because each call to GetProperties() is a cross process hop, and if you
|
||||
// marshal-by-value the property set, then you don't want to throw it away and
|
||||
// re-marshal it for every property. MAKE SURE YOU CACHE IT.
|
||||
if (props?.TryGetValue("FontFamily", out var family) ?? false)
|
||||
{
|
||||
FontFamily = family as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
|
||||
|
||||
public FiltersViewModel? Filters { get; set; }
|
||||
|
||||
private ObservableCollection<ListItemViewModel> Items { get; set; } = [];
|
||||
|
||||
private readonly ExtensionObject<IListPage> _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.
|
||||
/// </summary>
|
||||
private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, Filter));
|
||||
private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, SearchTextBox));
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -97,14 +97,27 @@ public partial class AliasManager : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
// Look for the old alias, and remove it
|
||||
List<CommandAlias> 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)
|
||||
|
||||
@@ -153,6 +153,11 @@ public sealed class CommandProviderWrapper
|
||||
// On a BG thread here
|
||||
fallbacks = model.FallbackCommands();
|
||||
|
||||
if (model is ICommandProvider2 two)
|
||||
{
|
||||
UnsafePreCacheApiAdditions(two);
|
||||
}
|
||||
|
||||
Id = model.Id;
|
||||
DisplayName = model.DisplayName;
|
||||
Icon = new(model.Icon);
|
||||
@@ -203,6 +208,19 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
|
||||
{
|
||||
var apiExtensions = provider.GetApiExtensionStubs();
|
||||
Logger.LogDebug($"Provider supports {apiExtensions.Length} extensions");
|
||||
foreach (var a in apiExtensions)
|
||||
{
|
||||
if (a is IExtendedAttributesProvider command2)
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
|
||||
|
||||
public override int GetHashCode() => _commandProvider.GetHashCode();
|
||||
|
||||
@@ -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 ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -19,6 +20,10 @@ internal sealed partial class FallbackLogItem : FallbackCommandItem
|
||||
Title = string.Empty;
|
||||
_logMessagesPage.Name = string.Empty;
|
||||
Subtitle = Properties.Resources.builtin_log_subtitle;
|
||||
|
||||
var logPath = Logger.LogDirectoryPath("\\CmdPal\\Logs\\");
|
||||
var openLogCommand = new OpenFileCommand(logPath) { Name = Resources.builtin_log_folder_command_name };
|
||||
MoreCommands = [new CommandContextItem(openLogCommand)];
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
|
||||
@@ -27,7 +27,9 @@ public partial class MainListPage : DynamicListPage,
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private readonly TopLevelCommandManager _tlcManager;
|
||||
private IEnumerable<IListItem>? _filteredItems;
|
||||
private IEnumerable<Scored<IListItem>>? _filteredItems;
|
||||
private IEnumerable<Scored<IListItem>>? _filteredApps;
|
||||
private IEnumerable<IListItem>? _allApps;
|
||||
private bool _includeApps;
|
||||
private bool _filteredItemsIncludesApps;
|
||||
|
||||
@@ -83,7 +85,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
|
||||
RaiseItemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +150,13 @@ public partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
return _filteredItems?.ToArray() ?? [];
|
||||
var items = Enumerable.Empty<Scored<IListItem>>()
|
||||
.Concat(_filteredItems is not null ? _filteredItems : [])
|
||||
.Concat(_filteredApps is not null ? _filteredApps : [])
|
||||
.OrderByDescending(o => o.Score)
|
||||
.Select(s => s.Item)
|
||||
.ToArray();
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,6 +175,8 @@ public partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_allApps = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +194,8 @@ public partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_allApps = null;
|
||||
RaiseItemsChanged(commands.Count);
|
||||
return;
|
||||
}
|
||||
@@ -193,35 +205,49 @@ public partial class MainListPage : DynamicListPage,
|
||||
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_allApps = null;
|
||||
}
|
||||
|
||||
// If the internal state has changed, reset _filteredItems to reset the list.
|
||||
if (_filteredItemsIncludesApps != _includeApps)
|
||||
{
|
||||
_filteredItems = null;
|
||||
_filteredApps = null;
|
||||
_allApps = null;
|
||||
}
|
||||
|
||||
var newFilteredItems = _filteredItems?.Select(s => s.Item);
|
||||
|
||||
// If we don't have any previous filter results to work with, start
|
||||
// with a list of all our commands & apps.
|
||||
if (_filteredItems is null)
|
||||
if (newFilteredItems is null && _filteredApps is null)
|
||||
{
|
||||
_filteredItems = commands;
|
||||
newFilteredItems = commands;
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
|
||||
if (_includeApps)
|
||||
{
|
||||
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems();
|
||||
var appIds = apps.Select(app => app.Command.Id).ToArray();
|
||||
|
||||
// Remove any top level pinned apps and use the apps from AllAppsCommandProvider.Page.GetItems()
|
||||
// since they contain details.
|
||||
_filteredItems = _filteredItems.Where(item => item.Command is not AppCommand);
|
||||
_filteredItems = _filteredItems.Concat(apps);
|
||||
_allApps = AllAppsCommandProvider.Page.GetItems();
|
||||
}
|
||||
}
|
||||
|
||||
// Produce a list of everything that matches the current filter.
|
||||
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem);
|
||||
RaiseItemsChanged(_filteredItems.Count());
|
||||
_filteredItems = ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, ScoreTopLevelItem);
|
||||
|
||||
// Produce a list of filtered apps with the appropriate limit
|
||||
if (_allApps is not null)
|
||||
{
|
||||
_filteredApps = ListHelpers.FilterListWithScores<IListItem>(_allApps, SearchText, ScoreTopLevelItem);
|
||||
|
||||
var appResultLimit = AllAppsCommandProvider.TopLevelResultLimit;
|
||||
if (appResultLimit >= 0)
|
||||
{
|
||||
_filteredApps = _filteredApps.Take(appResultLimit);
|
||||
}
|
||||
}
|
||||
|
||||
RaiseItemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using AdaptiveCards.ObjectModel.WinUI3;
|
||||
using AdaptiveCards.Templating;
|
||||
@@ -96,109 +97,40 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
UpdateProperty(nameof(Card));
|
||||
}
|
||||
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveSubmitAction))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveExecuteAction))]
|
||||
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
|
||||
{
|
||||
// BODGY circa GH #40979
|
||||
// Usually, you're supposed to try to cast the action to a specific
|
||||
// type, and use those objects to get the data you need.
|
||||
// However, there's something weird with AdaptiveCards and the way it
|
||||
// works when we consume it when built in Release, with AOT (and
|
||||
// trimming) enabled. Any sort of `action.As<IAdaptiveSubmitAction>()`
|
||||
// or similar will throw a System.InvalidCastException.
|
||||
//
|
||||
// Instead we have this horror show.
|
||||
//
|
||||
// The `action.ToJson()` blob ACTUALLY CONTAINS THE `type` field, which
|
||||
// we can use to determine what kind of action it is. Then we can parse
|
||||
// the JSON manually based on the type.
|
||||
var actionJson = action.ToJson();
|
||||
|
||||
if (actionJson.TryGetValue("type", out var actionTypeValue))
|
||||
if (action is AdaptiveOpenUrlAction openUrlAction)
|
||||
{
|
||||
var actionTypeString = actionTypeValue.GetString();
|
||||
Logger.LogTrace($"atString={actionTypeString}");
|
||||
|
||||
var actionType = actionTypeString switch
|
||||
{
|
||||
"Action.Submit" => ActionType.Submit,
|
||||
"Action.Execute" => ActionType.Execute,
|
||||
"Action.OpenUrl" => ActionType.OpenUrl,
|
||||
_ => ActionType.Unsupported,
|
||||
};
|
||||
|
||||
Logger.LogDebug($"{actionTypeString}->{actionType}");
|
||||
|
||||
switch (actionType)
|
||||
{
|
||||
case ActionType.OpenUrl:
|
||||
{
|
||||
HandleOpenUrlAction(action, actionJson);
|
||||
}
|
||||
|
||||
break;
|
||||
case ActionType.Submit:
|
||||
case ActionType.Execute:
|
||||
{
|
||||
HandleSubmitAction(action, actionJson, inputs);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
Logger.LogError($"{actionType} was an unexpected action `type`");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"actionJson.TryGetValue(type) failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleOpenUrlAction(IAdaptiveActionElement action, JsonObject actionJson)
|
||||
{
|
||||
if (actionJson.TryGetValue("url", out var actionUrlValue))
|
||||
{
|
||||
var actionUrl = actionUrlValue.GetString() ?? string.Empty;
|
||||
if (Uri.TryCreate(actionUrl, default(UriCreationOptions), out var uri))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(uri));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Failed to produce URI for {actionUrlValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSubmitAction(
|
||||
IAdaptiveActionElement action,
|
||||
JsonObject actionJson,
|
||||
JsonObject inputs)
|
||||
{
|
||||
var dataString = string.Empty;
|
||||
if (actionJson.TryGetValue("data", out var actionDataValue))
|
||||
{
|
||||
dataString = actionDataValue.Stringify() ?? string.Empty;
|
||||
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
|
||||
return;
|
||||
}
|
||||
|
||||
var inputString = inputs.Stringify();
|
||||
_ = Task.Run(() =>
|
||||
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
|
||||
{
|
||||
try
|
||||
// Get the data and inputs
|
||||
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
|
||||
var inputString = inputs.Stringify();
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
var model = _formModel.Unsafe!;
|
||||
if (model != null)
|
||||
try
|
||||
{
|
||||
var result = model.SubmitForm(inputString, dataString);
|
||||
Logger.LogDebug($"SubmitForm() returned {result}");
|
||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||
var model = _formModel.Unsafe!;
|
||||
if (model != null)
|
||||
{
|
||||
var result = model.SubmitForm(inputString, dataString);
|
||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
});
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string ErrorCardJson = """
|
||||
|
||||
@@ -126,6 +126,10 @@ public class ExtensionWrapper : IExtensionWrapper
|
||||
// We'll just return out nothing.
|
||||
return;
|
||||
}
|
||||
else if (hr.Value != 0)
|
||||
{
|
||||
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr.Value}");
|
||||
}
|
||||
|
||||
// Marshal.ThrowExceptionForHR(hr);
|
||||
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);
|
||||
|
||||
@@ -285,6 +285,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to View log folder.
|
||||
/// </summary>
|
||||
public static string builtin_log_folder_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_log_folder_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to View log.
|
||||
/// </summary>
|
||||
|
||||
@@ -135,6 +135,9 @@
|
||||
<data name="builtin_log_title" xml:space="preserve">
|
||||
<value>View log</value>
|
||||
</data>
|
||||
<data name="builtin_log_folder_command_name" xml:space="preserve">
|
||||
<value>View log folder</value>
|
||||
</data>
|
||||
<data name="builtin_reload_subtitle" xml:space="preserve">
|
||||
<value>Reload Command Palette extensions</value>
|
||||
</data>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,14 +41,18 @@
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||
</Flyout>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for context item separators -->
|
||||
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorContextItemViewModel">
|
||||
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-16,-12,-12,-12"
|
||||
|
||||
@@ -270,7 +270,7 @@ public sealed partial class ContextMenu : UserControl,
|
||||
|
||||
private bool IsSeparator(object item)
|
||||
{
|
||||
return item is SeparatorContextItemViewModel;
|
||||
return item is SeparatorViewModel;
|
||||
}
|
||||
|
||||
private void UpdateUiForStackChange()
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.FiltersDropDown"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Background="Transparent"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
|
||||
<cmdpalUI:FilterTemplateSelector
|
||||
x:Key="FilterTemplateSelector"
|
||||
Default="{StaticResource FilterItemViewModelTemplate}"
|
||||
Separator="{StaticResource SeparatorViewModelTemplate}" />
|
||||
|
||||
<Style
|
||||
x:Name="ComboBoxStyle"
|
||||
BasedOn="{StaticResource DefaultComboBoxStyle}"
|
||||
TargetType="ComboBox">
|
||||
<Style.Setters>
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Setter Property="Margin" Value="0,0,12,0" />
|
||||
<Setter Property="Padding" Value="16,4" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Template for the filter items -->
|
||||
<DataTemplate x:Key="FilterItemViewModelTemplate" x:DataType="coreViewModels:FilterItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Name, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<cpcontrols:IconBox
|
||||
Width="16"
|
||||
Margin="4,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SourceKey="{x:Bind Icon}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for separators -->
|
||||
<DataTemplate x:Key="SeparatorViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-16,-12,-12,-12"
|
||||
Fill="{ThemeResource MenuFlyoutSeparatorThemeBrush}" />
|
||||
</DataTemplate>
|
||||
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<ComboBox
|
||||
Name="FiltersComboBox"
|
||||
x:Uid="FiltersComboBox"
|
||||
VerticalAlignment="Center"
|
||||
ItemTemplateSelector="{StaticResource FilterTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.Filters, Mode=OneWay}"
|
||||
PlaceholderText="Filters"
|
||||
PreviewKeyDown="FiltersComboBox_PreviewKeyDown"
|
||||
SelectionChanged="FiltersComboBox_SelectionChanged"
|
||||
Style="{StaticResource ComboBoxStyle}"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultComboBoxItemStyle}" TargetType="ComboBoxItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,8" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ComboBox.ItemContainerTransitions>
|
||||
</ComboBox>
|
||||
</UserControl>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -162,11 +163,11 @@ public sealed partial class ListPage : Page,
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
var notificationText = li.Title;
|
||||
listViewPeer.RaiseNotificationEvent(
|
||||
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationKind.Other,
|
||||
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationProcessing.MostRecent,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(icon.Icon))
|
||||
{
|
||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false);
|
||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false, icon.FontFamily);
|
||||
return source;
|
||||
}
|
||||
else if (icon.Data is not null)
|
||||
|
||||
@@ -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
|
||||
|
||||
32
src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/UIHelper.cs
Normal file
32
src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/UIHelper.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
public static partial class UIHelper
|
||||
{
|
||||
static UIHelper()
|
||||
{
|
||||
}
|
||||
|
||||
public static void AnnounceActionForAccessibility(UIElement ue, string announcement, string activityID)
|
||||
{
|
||||
if (FrameworkElementAutomationPeer.FromElement(ue) is AutomationPeer peer)
|
||||
{
|
||||
peer.RaiseNotificationEvent(
|
||||
AutomationNotificationKind.ActionCompleted,
|
||||
AutomationNotificationProcessing.ImportantMostRecent,
|
||||
announcement,
|
||||
activityID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,12 @@
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
|
||||
<!-- <PropertyGroup>
|
||||
<EnableCmdPalAOT>true</EnableCmdPalAOT>
|
||||
<CIBuild>true</CIBuild>
|
||||
</PropertyGroup> -->
|
||||
|
||||
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
|
||||
<SelfContained>true</SelfContained>
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Back button -->
|
||||
@@ -320,6 +321,18 @@
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
</Grid>
|
||||
<!-- Filter: wrapped in a grid to enable RepositionThemeTransitions -->
|
||||
<Grid Grid.Column="2" HorizontalAlignment="Right">
|
||||
<cpcontrols:FiltersDropDown
|
||||
x:Name="FiltersDropDown"
|
||||
HorizontalAlignment="Right"
|
||||
CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
|
||||
<Grid.Transitions>
|
||||
<TransitionCollection>
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<ProgressBar
|
||||
|
||||
@@ -25,17 +25,11 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- TO DO: Replace this with WinUI TitleBar once that ships. -->
|
||||
<Button
|
||||
x:Name="PaneToggleBtn"
|
||||
Width="48"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Click="PaneToggleBtn_Click"
|
||||
Style="{StaticResource PaneToggleButtonStyle}" />
|
||||
<StackPanel
|
||||
x:Name="AppTitleBar"
|
||||
Grid.Row="0"
|
||||
Height="48"
|
||||
Margin="16,0,0,0"
|
||||
Orientation="Horizontal">
|
||||
<Image
|
||||
Width="16"
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using WinUIEx;
|
||||
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
||||
@@ -22,6 +23,9 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
{
|
||||
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
|
||||
|
||||
// Gets or sets optional action invoked after NavigationView is loaded.
|
||||
public Action NavigationViewLoaded { get; set; } = () => { };
|
||||
|
||||
public SettingsWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
@@ -35,10 +39,33 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
}
|
||||
|
||||
// Handles NavigationView loaded event.
|
||||
// Sets up initial navigation and accessibility notifications.
|
||||
private void NavView_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Delay necessary to ensure NavigationView visual state can match navigation
|
||||
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
NavView.SelectedItem = NavView.MenuItems[0];
|
||||
Navigate("General");
|
||||
|
||||
if (sender is NavigationView navigationView)
|
||||
{
|
||||
// Register for pane open/close changes to announce to screen readers
|
||||
navigationView.RegisterPropertyChangedCallback(NavigationView.IsPaneOpenProperty, AnnounceNavigationPaneStateChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// Announces navigation pane open/close state to screen readers for accessibility.
|
||||
private void AnnounceNavigationPaneStateChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
if (sender is NavigationView navigationView)
|
||||
{
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ue: (UIElement)sender,
|
||||
(sender as NavigationView)?.IsPaneOpen == true ? RS_.GetString("NavigationPaneOpened") : RS_.GetString("NavigationPaneClosed"),
|
||||
"NavigationViewPaneIsOpenChangeNotificationId");
|
||||
}
|
||||
}
|
||||
|
||||
private void NavView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args)
|
||||
@@ -109,24 +136,15 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
}
|
||||
|
||||
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
NavView.IsPaneOpen = !NavView.IsPaneOpen;
|
||||
}
|
||||
|
||||
private void NavView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||
{
|
||||
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
|
||||
{
|
||||
PaneToggleBtn.Visibility = Visibility.Visible;
|
||||
NavView.IsPaneToggleButtonVisible = false;
|
||||
AppTitleBar.Margin = new Thickness(48, 0, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
PaneToggleBtn.Visibility = Visibility.Collapsed;
|
||||
NavView.IsPaneToggleButtonVisible = true;
|
||||
AppTitleBar.Margin = new Thickness(16, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -419,8 +419,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="TrayMenu_Settings" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="TrayMenu_Exit" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
<data name="TrayMenu_Close" xml:space="preserve">
|
||||
<value>Close</value>
|
||||
<comment>Close as a verb, as in Close the application</comment>
|
||||
</data>
|
||||
<data name="Settings_ExtensionPage_Alias_ToggleSwitch.OnContent" xml:space="preserve">
|
||||
<value>Direct</value>
|
||||
@@ -434,4 +435,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="StatusMessagesButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Show status messages</value>
|
||||
</data>
|
||||
<data name="NavigationPaneClosed" xml:space="preserve">
|
||||
<value>Navigation pane closed</value>
|
||||
</data>
|
||||
<data name="NavigationPaneOpened" xml:space="preserve">
|
||||
<value>Navigation page opened</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -158,7 +158,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
// Return Value:
|
||||
// - An IconElement with its IconSource set, if possible.
|
||||
template<typename TIconSource>
|
||||
TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const int targetSize)
|
||||
TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
||||
{
|
||||
TIconSource iconSource{ nullptr };
|
||||
|
||||
@@ -187,6 +187,11 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
|
||||
}
|
||||
else if (!fontFamily.empty())
|
||||
{
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ fontFamily });
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: you _do_ need to manually set the font here.
|
||||
@@ -225,9 +230,9 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false);
|
||||
// }
|
||||
|
||||
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const int targetSize)
|
||||
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
||||
{
|
||||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, targetSize);
|
||||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, fontFamily, targetSize);
|
||||
}
|
||||
|
||||
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
|
||||
@@ -343,13 +348,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
|
||||
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
|
||||
const bool monochrome,
|
||||
const winrt::hstring& fontFamily,
|
||||
const int targetSize)
|
||||
{
|
||||
std::wstring_view iconPathWithoutIndex;
|
||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||
if (!indexOpt.has_value())
|
||||
{
|
||||
return _IconSourceMUX(iconPath, monochrome, targetSize);
|
||||
return _IconSourceMUX(iconPath, monochrome, fontFamily, targetSize);
|
||||
}
|
||||
|
||||
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize);
|
||||
@@ -369,7 +375,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||
if (!indexOpt.has_value())
|
||||
{
|
||||
auto source = IconSourceMUX(iconPath, false, targetSize);
|
||||
auto source = IconSourceMUX(iconPath, false, L"", targetSize);
|
||||
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
|
||||
icon.IconSource(source);
|
||||
return icon;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
|
||||
//static Windows::UI::Xaml::Controls::IconElement IconWUX(const winrt::hstring& iconPath);
|
||||
//static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(const winrt::hstring& iconPath);
|
||||
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const int targetSize=24);
|
||||
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const winrt::hstring& fontFamily, const int targetSize=24);
|
||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath);
|
||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Microsoft.Terminal.UI
|
||||
{
|
||||
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
|
||||
// static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale);
|
||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale, String fontFamily);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user