mirror of
https://github.com/microsoft/PowerToys
synced 2025-08-22 01:58:04 +00:00
Merge branch 'main' into gleb/screen-ruler-uitests
This commit is contained in:
commit
12c84b19c9
38
.github/actions/spell-check/expect.txt
vendored
38
.github/actions/spell-check/expect.txt
vendored
@ -25,6 +25,8 @@ ADMINS
|
|||||||
adml
|
adml
|
||||||
admx
|
admx
|
||||||
advancedpaste
|
advancedpaste
|
||||||
|
advancedpasteui
|
||||||
|
advancedpasteuishortcut
|
||||||
advfirewall
|
advfirewall
|
||||||
AFeature
|
AFeature
|
||||||
affordances
|
affordances
|
||||||
@ -40,6 +42,7 @@ ALLINPUT
|
|||||||
Allman
|
Allman
|
||||||
Allmodule
|
Allmodule
|
||||||
ALLOWUNDO
|
ALLOWUNDO
|
||||||
|
allpc
|
||||||
ALLVIEW
|
ALLVIEW
|
||||||
ALPHATYPE
|
ALPHATYPE
|
||||||
AModifier
|
AModifier
|
||||||
@ -115,6 +118,7 @@ bigbar
|
|||||||
bigobj
|
bigobj
|
||||||
binlog
|
binlog
|
||||||
binres
|
binres
|
||||||
|
binskim
|
||||||
BITMAPFILEHEADER
|
BITMAPFILEHEADER
|
||||||
bitmapimage
|
bitmapimage
|
||||||
BITMAPINFO
|
BITMAPINFO
|
||||||
@ -255,6 +259,7 @@ Corpor
|
|||||||
cotaskmem
|
cotaskmem
|
||||||
COULDNOT
|
COULDNOT
|
||||||
countof
|
countof
|
||||||
|
covrun
|
||||||
cpcontrols
|
cpcontrols
|
||||||
cph
|
cph
|
||||||
cplusplus
|
cplusplus
|
||||||
@ -627,6 +632,7 @@ HKCU
|
|||||||
hkey
|
hkey
|
||||||
HKLM
|
HKLM
|
||||||
HKM
|
HKM
|
||||||
|
hkmng
|
||||||
HKPD
|
HKPD
|
||||||
HKU
|
HKU
|
||||||
HMD
|
HMD
|
||||||
@ -635,6 +641,7 @@ hmodule
|
|||||||
hmonitor
|
hmonitor
|
||||||
homies
|
homies
|
||||||
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
||||||
|
HOOKPROC
|
||||||
HORZRES
|
HORZRES
|
||||||
HORZSIZE
|
HORZSIZE
|
||||||
Hostbackdropbrush
|
Hostbackdropbrush
|
||||||
@ -643,7 +650,11 @@ Hostx
|
|||||||
hotfixes
|
hotfixes
|
||||||
hotkeycontrol
|
hotkeycontrol
|
||||||
HOTKEYF
|
HOTKEYF
|
||||||
|
hotkeylockmachine
|
||||||
|
hotkeyreconnect
|
||||||
hotkeys
|
hotkeys
|
||||||
|
hotkeyswitch
|
||||||
|
hotkeytoggleeasymouse
|
||||||
hotlight
|
hotlight
|
||||||
hotspot
|
hotspot
|
||||||
HPAINTBUFFER
|
HPAINTBUFFER
|
||||||
@ -701,9 +712,12 @@ IMAGERESIZERCONTEXTMENU
|
|||||||
IMAGERESIZEREXT
|
IMAGERESIZEREXT
|
||||||
imageresizerinput
|
imageresizerinput
|
||||||
imageresizersettings
|
imageresizersettings
|
||||||
|
imagetotext
|
||||||
|
imagetotextshortcut
|
||||||
imagingdevices
|
imagingdevices
|
||||||
ime
|
ime
|
||||||
imgflip
|
imgflip
|
||||||
|
inapp
|
||||||
inbox
|
inbox
|
||||||
INCONTACT
|
INCONTACT
|
||||||
Indo
|
Indo
|
||||||
@ -757,6 +771,7 @@ istep
|
|||||||
ith
|
ith
|
||||||
ITHUMBNAIL
|
ITHUMBNAIL
|
||||||
IUI
|
IUI
|
||||||
|
IUWP
|
||||||
IWIC
|
IWIC
|
||||||
jfif
|
jfif
|
||||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||||
@ -786,6 +801,7 @@ keyvault
|
|||||||
KILLFOCUS
|
KILLFOCUS
|
||||||
killrunner
|
killrunner
|
||||||
kmph
|
kmph
|
||||||
|
kvp
|
||||||
Kybd
|
Kybd
|
||||||
lastcodeanalysissucceeded
|
lastcodeanalysissucceeded
|
||||||
LASTEXITCODE
|
LASTEXITCODE
|
||||||
@ -824,6 +840,7 @@ localappdata
|
|||||||
localpackage
|
localpackage
|
||||||
LOCALSYSTEM
|
LOCALSYSTEM
|
||||||
LOCATIONCHANGE
|
LOCATIONCHANGE
|
||||||
|
LOCKMACHINE
|
||||||
LOCKTYPE
|
LOCKTYPE
|
||||||
LOGFONT
|
LOGFONT
|
||||||
LOGFONTW
|
LOGFONTW
|
||||||
@ -909,6 +926,7 @@ MDL
|
|||||||
mdtext
|
mdtext
|
||||||
mdtxt
|
mdtxt
|
||||||
mdwn
|
mdwn
|
||||||
|
measuretool
|
||||||
meme
|
meme
|
||||||
memicmp
|
memicmp
|
||||||
MENUITEMINFO
|
MENUITEMINFO
|
||||||
@ -958,6 +976,7 @@ MOUSEHWHEEL
|
|||||||
MOUSEINPUT
|
MOUSEINPUT
|
||||||
mousejump
|
mousejump
|
||||||
mousepointer
|
mousepointer
|
||||||
|
mousepointercrosshairs
|
||||||
mouseutils
|
mouseutils
|
||||||
MOVESIZEEND
|
MOVESIZEEND
|
||||||
MOVESIZESTART
|
MOVESIZESTART
|
||||||
@ -969,6 +988,7 @@ msc
|
|||||||
mscorlib
|
mscorlib
|
||||||
msctls
|
msctls
|
||||||
msdata
|
msdata
|
||||||
|
msdia
|
||||||
MSDL
|
MSDL
|
||||||
MSGFLT
|
MSGFLT
|
||||||
MSHCTX
|
MSHCTX
|
||||||
@ -1157,6 +1177,18 @@ PARENTRELATIVEFORADDRESSBAR
|
|||||||
PARENTRELATIVEPARSING
|
PARENTRELATIVEPARSING
|
||||||
parray
|
parray
|
||||||
PARTIALCONFIRMATIONDIALOGTITLE
|
PARTIALCONFIRMATIONDIALOGTITLE
|
||||||
|
pasteashtmlfile
|
||||||
|
pasteashtmlfileshortcut
|
||||||
|
pasteasjson
|
||||||
|
pasteasjsonshortcut
|
||||||
|
pasteasmarkdown
|
||||||
|
pasteasmarkdownshortcut
|
||||||
|
pasteasplaintext
|
||||||
|
pasteasplaintextshortcut
|
||||||
|
pasteaspngfile
|
||||||
|
pasteaspngfileshortcut
|
||||||
|
pasteastxtfile
|
||||||
|
pasteastxtfileshortcut
|
||||||
PATCOPY
|
PATCOPY
|
||||||
PATHMUSTEXIST
|
PATHMUSTEXIST
|
||||||
PATINVERT
|
PATINVERT
|
||||||
@ -1224,6 +1256,7 @@ Pomodoro
|
|||||||
Popups
|
Popups
|
||||||
POPUPWINDOW
|
POPUPWINDOW
|
||||||
POSITIONITEM
|
POSITIONITEM
|
||||||
|
powerocr
|
||||||
POWERRENAMECONTEXTMENU
|
POWERRENAMECONTEXTMENU
|
||||||
powerrenameinput
|
powerrenameinput
|
||||||
POWERRENAMETEST
|
POWERRENAMETEST
|
||||||
@ -1364,6 +1397,7 @@ Removelnk
|
|||||||
renamable
|
renamable
|
||||||
RENAMEONCOLLISION
|
RENAMEONCOLLISION
|
||||||
reparented
|
reparented
|
||||||
|
reparenthotkey
|
||||||
reparenting
|
reparenting
|
||||||
reportfileaccesses
|
reportfileaccesses
|
||||||
requery
|
requery
|
||||||
@ -1613,6 +1647,7 @@ STYLECHANGED
|
|||||||
STYLECHANGING
|
STYLECHANGING
|
||||||
subkeys
|
subkeys
|
||||||
sublang
|
sublang
|
||||||
|
Subdomain
|
||||||
SUBMODULEUPDATE
|
SUBMODULEUPDATE
|
||||||
subresource
|
subresource
|
||||||
Superbar
|
Superbar
|
||||||
@ -1683,6 +1718,7 @@ THH
|
|||||||
THICKFRAME
|
THICKFRAME
|
||||||
THISCOMPONENT
|
THISCOMPONENT
|
||||||
throughs
|
throughs
|
||||||
|
thumbnailhotkey
|
||||||
TILEDWINDOW
|
TILEDWINDOW
|
||||||
TILLSON
|
TILLSON
|
||||||
timedate
|
timedate
|
||||||
@ -1697,6 +1733,7 @@ tlb
|
|||||||
tlbimp
|
tlbimp
|
||||||
tlc
|
tlc
|
||||||
TNP
|
TNP
|
||||||
|
TOGGLEEASYMOUSE
|
||||||
Toolhelp
|
Toolhelp
|
||||||
toolkitconverters
|
toolkitconverters
|
||||||
toolwindow
|
toolwindow
|
||||||
@ -1710,6 +1747,7 @@ tracelogging
|
|||||||
tracerpt
|
tracerpt
|
||||||
trackbar
|
trackbar
|
||||||
trafficmanager
|
trafficmanager
|
||||||
|
transcodetomp
|
||||||
transicc
|
transicc
|
||||||
TRAYMOUSEMESSAGE
|
TRAYMOUSEMESSAGE
|
||||||
triaging
|
triaging
|
||||||
|
2
.github/workflows/msstore-submissions.yml
vendored
2
.github/workflows/msstore-submissions.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: BODGY - Set up Gnome Keyring for future Cert Auth
|
- name: BODGY - Set up Gnome Keyring for future Cert Auth
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt-get install -y gnome-keyring
|
sudo apt-get update && sudo apt-get install -y gnome-keyring
|
||||||
export $(dbus-launch --sh-syntax)
|
export $(dbus-launch --sh-syntax)
|
||||||
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --unlock)
|
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --unlock)
|
||||||
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh)
|
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh)
|
||||||
|
@ -64,6 +64,10 @@ extends:
|
|||||||
tsa:
|
tsa:
|
||||||
enabled: true
|
enabled: true
|
||||||
configFile: '$(Build.SourcesDirectory)\.pipelines\tsa.json'
|
configFile: '$(Build.SourcesDirectory)\.pipelines\tsa.json'
|
||||||
|
binskim:
|
||||||
|
enabled: true
|
||||||
|
# Exclude every dll/exe in tests/*, as well as all msdia*, covrun* and vcruntime*
|
||||||
|
analyzeTargetGlob: +:file|$(Build.ArtifactStagingDirectory)/**/*.dll;+:file|$(Build.ArtifactStagingDirectory)/**/*.exe;-:file:regex|tests.*\.(dll|exe)$;-:file:regex|(covrun.*)\.dll$;-:file:regex|(msdia.*)\.dll$;-:file:regex|(vcruntime.*)\.dll$
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- stage: Build
|
- stage: Build
|
||||||
|
@ -792,6 +792,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|ARM64 = Debug|ARM64
|
Debug|ARM64 = Debug|ARM64
|
||||||
@ -2854,18 +2858,22 @@ Global
|
|||||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
|
{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.ActiveCfg = Release|x64
|
||||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
|
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.Build.0 = Debug|ARM64
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.Deploy.0 = Debug|ARM64
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.ActiveCfg = Debug|x64
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.Build.0 = Debug|x64
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.Deploy.0 = Debug|x64
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.ActiveCfg = Release|ARM64
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Build.0 = Release|ARM64
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Deploy.0 = Release|ARM64
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.ActiveCfg = Release|x64
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Build.0 = Release|x64
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Deploy.0 = Release|x64
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||||
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -3177,8 +3185,8 @@ Global
|
|||||||
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {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}
|
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||||
{E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B}
|
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||||
{66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1}
|
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||||
|
202
README.md
202
README.md
@ -14,7 +14,7 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
|
|||||||
| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
|
| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
|
||||||
| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
|
| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
|
||||||
| [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
|
| [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
|
||||||
| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
|
| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
|
||||||
| [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) |
|
| [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) |
|
||||||
| [Peek](https://aka.ms/PowerToysOverview_Peek) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
|
| [Peek](https://aka.ms/PowerToysOverview_Peek) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
|
||||||
| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
|
| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
|
||||||
@ -35,19 +35,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
|
|||||||
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
|
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
|
||||||
|
|
||||||
<!-- items that need to be updated release to release -->
|
<!-- items that need to be updated release to release -->
|
||||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22
|
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
|
||||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.92%22
|
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22
|
||||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysUserSetup-0.92.1-x64.exe
|
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-x64.exe
|
||||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysUserSetup-0.92.1-arm64.exe
|
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-arm64.exe
|
||||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysSetup-0.92.1-x64.exe
|
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-x64.exe
|
||||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysSetup-0.92.1-arm64.exe
|
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-arm64.exe
|
||||||
|
|
||||||
| Description | Filename |
|
| Description | Filename |
|
||||||
|----------------|----------|
|
|----------------|----------|
|
||||||
| Per user - x64 | [PowerToysUserSetup-0.92.1-x64.exe][ptUserX64] |
|
| Per user - x64 | [PowerToysUserSetup-0.93.0-x64.exe][ptUserX64] |
|
||||||
| Per user - ARM64 | [PowerToysUserSetup-0.92.1-arm64.exe][ptUserArm64] |
|
| Per user - ARM64 | [PowerToysUserSetup-0.93.0-arm64.exe][ptUserArm64] |
|
||||||
| Machine wide - x64 | [PowerToysSetup-0.92.1-x64.exe][ptMachineX64] |
|
| Machine wide - x64 | [PowerToysSetup-0.93.0-x64.exe][ptMachineX64] |
|
||||||
| Machine wide - ARM64 | [PowerToysSetup-0.92.1-arm64.exe][ptMachineArm64] |
|
| Machine wide - ARM64 | [PowerToysSetup-0.93.0-arm64.exe][ptMachineArm64] |
|
||||||
|
|
||||||
This is our preferred method.
|
This is our preferred method.
|
||||||
|
|
||||||
@ -93,139 +93,119 @@ For guidance on developing for PowerToys, please read the [developer docs](./doc
|
|||||||
|
|
||||||
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
|
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
|
||||||
|
|
||||||
### 0.92 - June 2025 Update
|
### 0.93 - Aug 2025 Update
|
||||||
|
|
||||||
In this release, we focused on new features, stability, optimization improvements, and automation.
|
In this release, we focused on new features, stability, optimization improvements, and automation.
|
||||||
|
|
||||||
**✨Highlights**
|
**✨Highlights**
|
||||||
|
|
||||||
- PowerToys settings now has a toggle for the system tray icon, giving users control over its visibility based on personal preference. Thanks [@BLM16](https://github.com/BLM16)!
|
- PowerToys settings debuts a modern, card-based dashboard with clearer descriptions and faster navigation for a streamlined user experience.
|
||||||
- Command Palette now has Ahead-of-Time ([AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot)) compatibility for all first-party extensions, improved extensibility, and core UX fixes, resulting in better performance and stability across commands.
|
- Command Palette had over 99 issues resolved, including bringing back Clipboard History, adding context menu shortcuts, pinning favorite apps, and supporting history in Run.
|
||||||
- Color Picker now has customizable mouse button actions, enabling more personalized workflows by assigning functions to left, right, and middle clicks. Thanks [@PesBandi](https://github.com/PesBandi)!
|
- Command Palette reduced its startup memory usage by ~15%, load time by ~40%, built-in extensions loading time by ~70%, and installation size by ~55%—all due to using the full Ahead-of-Time (AOT) compilation mode in Windows App SDK.
|
||||||
- Bug Report Tool now has a faster and clearer reporting process, with progress indicators, improved compression, auto-cleanup of old trace logs, and inclusion of MSIX installer logs for more efficient diagnostics.
|
- Peek now supports instant previews and embedded thumbnails for Binary G-code (.bgcode) 3D printing files, making it easy to inspect models at a glance. Thanks [@pedrolamas](https://github.com/pedrolamas)!
|
||||||
- File Explorer add-ons now have improved rendering stability, resolving issues with PDF previews, blank thumbnails, and text file crashes during file browsing.
|
- Mouse Utilities introduces a new spotlight highlighting mode that dims the screen and draws attention to your cursor, perfect for presentations.
|
||||||
|
- Test coverage improvements for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename — ensuring better reliability and quality, with over 600 new unit tests (mostly for Command Palette) and doubled UI automation coverage.
|
||||||
### Color Picker
|
|
||||||
|
|
||||||
- Added mouse button actions so you can choose what left, right, or middle click does. Thanks [@PesBandi](https://github.com/PesBandi)!
|
|
||||||
|
|
||||||
### Crop & Lock
|
|
||||||
|
|
||||||
- Aligned window styling with current Windows theme for a cleaner look. Thanks [@sadirano](https://github.com/sadirano)!
|
|
||||||
|
|
||||||
### Command Palette
|
### Command Palette
|
||||||
|
|
||||||
- Enhanced performance by resolving a regression in page loading.
|
- Ensured screen readers are notified when the selected item in the list changes for better accessibility.
|
||||||
- Applied consistent hotkey handling across all Command Palette commands for a smoother user experience.
|
- Fixed command title changes not being properly notified to screen readers. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Improved graceful closing of Command Palette. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
- Made icon controls excluded from keyboard navigation by default for better accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Fixed consistency issue for extensions' alias with "Direct" setting and enabled localization for "Direct" and "Indirect" for better user understanding. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
- Improved UI design with better text sizing and alignment.
|
||||||
- Improved visual clarity by styling critical context items correctly.
|
- Fixed keyboard shortcuts to work better in text boxes and context menus.
|
||||||
- Automatically focused the field when only one is present on the content page.
|
- Added right-click context menus with critical command styling and separators.
|
||||||
- Improved stability and efficiency when loading file icons in SDK ThumbnailHelper.cs by removing unnecessary operations. Thanks [@OldUser101](https://github.com/OldUser101)!
|
- Improved various context menu issues, improving item selection, handling of long titles, search bar text scaling, initial item behavior, and primary button functionality.
|
||||||
- Enhanced details view with commands implementation. (See [Extension sample](./src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs))
|
- Fixed context menu crashes with better type handling.
|
||||||
|
- Fixed "Reload" command to work with both uppercase and lowercase letters.
|
||||||
|
- Added mouse back button support for easier navigation. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Updated back button tooltip to show keyboard shortcut information. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Fixed Command Palette window not appearing properly when activated. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Fixed Command Palette window staying hidden from taskbar after File Explorer restarts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Fixed window focus not returning to previous app properly.
|
||||||
|
- Fixed Command Palette window to always appear on top when shown and move to bottom when hidden. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Fixed window hiding to properly work on UI thread. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Fixed crashes and improved stability with better synchronization of Command list updates. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Improved extension disposal with better error handling to prevent crashes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Improved stability by fixing a UI threading issue when loading more results, preventing possible crashes and ensuring the loading state resets if loading fails. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Enhanced icon loading stability with better exception handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Added thread safety to recent commands to prevent crashes. Thanks [@MaoShengelia](https://github.com/MaoShengelia)!
|
||||||
|
- Fixed acrylic (frosted glass) system backdrop display issues by ensuring proper UI thread handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
|
||||||
### Command Palette extensions
|
### Command Palette extensions
|
||||||
|
|
||||||
- Added "Copy Path" command to *App* search results for convenience. Thanks [@PesBandi](https://github.com/PesBandi)!
|
- Added settings to each provider to control which fallback commands are enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! for fixing a regression in this feature.
|
||||||
- Improved *Calculator* input experience by ignoring leading equal signs. Thanks [@PesBandi](https://github.com/PesBandi)!
|
- Added sample code showing how Command Palette extensions can track when their pages are loaded or unloaded. [Check it out here](./src/modules/cmdpal/ext/SamplePagesExtension/OnLoadPage.cs).
|
||||||
- Corrected input handling in the *Calculator* extension to avoid showing errors for input with only leading whitespace.
|
- Fixed *Calculator* to accept regular spaces in numbers that use space separators. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||||
- Improved *New Extension* wizard by validating names to prevent namespace errors.
|
- Added a new setting to *Calculator* to make "Copy" the primary button (replacing “Save”) and enable "Close on Enter", streamlining the workflow. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||||
- Ensured consistent context items display for the *Run* extension between fallback and top-level results.
|
- Improved *Apps* indexing error handling and removed obsolete code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||||
- Fixed missing *Time & Date* commands in fallback results. Thanks [@htcfreek](https://github.com/htcfreek)!
|
- Prevented apps from showing in search when the *Apps* extension is disabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
- Fixed outdated results in the *Time & Date* extension. Thanks [@htcfreek](https://github.com/htcfreek)!
|
- Added ability to pin/unpin *Apps* using Ctrl+P shortcut.
|
||||||
- Fixed an issue where *Web Search* always opened Microsoft Edge instead of the user's default browser on Windows 11 24H2 and later. Thanks [@RuggMatt](https://github.com/RuggMatt)!
|
- Added keyboard shortcuts to the *Apps* context menu items for faster access.
|
||||||
- Improved ordering of *Windows Settings* extension search results from alphabetical to relevance-based for quicker access.
|
- Added all file context menu options to the *Apps* items context menu, making all file actions available there for better functionality.
|
||||||
- Added "Restart Windows Explorer" command to the *Windows System Commands* provider for gracefully terminate and relaunch explorer.exe. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
- Streamlined All *Apps* extension settings by removing redundant descriptions, making the UI clearer.
|
||||||
|
- Added command history to the *Run* page for easier access to previous commands.
|
||||||
|
- Fixed directory path handling in *Run* fallback for better file navigation.
|
||||||
|
- Fixed URL fallback item hiding properly in *Web Search* extension when search query becomes invalid. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Added proper empty state message for *Web Search* extension when no results found. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||||
|
- Added fallback command to *Windows Settings* extension for better search results.
|
||||||
|
- Re-enabled *Clipboard History* feature with proper window handling.
|
||||||
|
- Improved *Add Bookmark* extension to automatically detect file, folder, or URL types without manual input.
|
||||||
|
- Updated terminology from "Kill process" to "End task" in *Window Walker* for consistency with Windows.
|
||||||
|
- Fixed minor grammar error in SamplePagesExtension code comments. Thanks [@purofle](https://github.com/purofle)!
|
||||||
|
|
||||||
### Command Palette Ahead-of-Time (AOT) readiness
|
### Mouse Utilities
|
||||||
|
|
||||||
- We’ve made foundational changes to prepare the Command Palette for future Ahead-of-Time (AOT) publishing. This includes replacing the calculator library with ExprTk, improving COM object handling, refining Win32 interop, and correcting trimming behavior—all to ensure compatibility, performance, and reliability under AOT constraints. All first-party extensions are now AOT-compatible. These improvements lay the groundwork for publishing Command Palette as an AOT application in the next release.
|
- Added a new spotlight highlighting mode that creates a large transparent circle around your cursor with a backdrop effect, providing an alternative to the traditional circle highlight. Perfect for presentations where you want to focus attention on a specific area while dimming the rest of the screen.
|
||||||
- Special thanks to [@Sergio0694](https://github.com/Sergio0694) for guidance on making COM APIs AOT-compatible, [@jtschuster](https://github.com/jtschuster) for fixing COM object handling, [@ArashPartow](https://github.com/ArashPartow) from ExprTk for integration suggestions, and [@tian-lt](https://github.com/tian-lt) from the Windows Calculator team for valuable suggestion throughout the migration journey and review.
|
|
||||||
- As part of the upcoming release, we’re also enabling AOT compatibility for key dependencies, including markdown rendering, Adaptive Cards, internal logging and telemetry library, and the core Command Palette UX.
|
|
||||||
|
|
||||||
### FancyZones
|
|
||||||
|
|
||||||
- Fixed DPI-scaling issues to ensure FancyZones Editor displays crisply on high-resolution monitors. Thanks [@HO-COOH](https://github.com/HO-COOH)! This inspired us a broader review across other PowerToys modules, leading to DPI display optimizations in Awake, Color Picker, PowerAccent, and more.
|
|
||||||
|
|
||||||
### File Explorer add-ons
|
|
||||||
|
|
||||||
- Fixed potential failures in PDF previewer and thumbnail generation, improving reliability when browsing PDF files. Thanks [@mohiuddin-khan-shiam](https://github.com/mohiuddin-khan-shiam)!
|
|
||||||
- Prevented Monaco Preview Handler crash when opening UTF-8-BOM text files.
|
|
||||||
|
|
||||||
### Hosts File Editor
|
|
||||||
|
|
||||||
- Added an in-app *“Learn more”* link to warning dialogs for quick guidance. Thanks [@PesBandi](https://github.com/PesBandi)!
|
|
||||||
|
|
||||||
### Mouse Without Borders
|
|
||||||
|
|
||||||
- Fixed firewall rule so MWB now accepts connections from IPs outside your local subnet.
|
|
||||||
- Cleaned legacy logs to reduce disk usage and noise.
|
|
||||||
|
|
||||||
### Peek
|
### Peek
|
||||||
|
|
||||||
- Updated QOI reader so 3-channel QOI images preview correctly in Peek and File Explorer. Thanks [@mbartlett21](https://github.com/mbartlett21)!
|
- Added preview and thumbnail support for Binary G-code (.bgcode) files used in 3D printing. You can now see embedded thumbnails and preview these compressed 3D printing files directly in Peek and File Explorer. Thanks [@pedrolamas](https://github.com/pedrolamas)!
|
||||||
- Added codec detection with a clear warning when a video can’t be previewed, along with a link to the Microsoft Store to download the required codec.
|
|
||||||
|
|
||||||
### PowerRename
|
### Quick Accent
|
||||||
|
|
||||||
- Added support for $YY-$MM-$DD in ModificationTime and AccessTime to enable flexible date-based renaming.
|
- Added Vietnamese language support to Quick Accent, mappings for Vietnamese vowels (a, e, i, o, u, y) and the letter d. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
|
||||||
|
|
||||||
### PowerToys Run
|
|
||||||
|
|
||||||
- Suppressed error UI for known WPF-related crashes to reduce user confusion, while retaining diagnostic logging for analysis. This targets COMException 0xD0000701 and 0x80263001 caused by temporary DWM unavailability.
|
|
||||||
|
|
||||||
### Registry Preview
|
|
||||||
|
|
||||||
- Added "Extended data preview" via magnifier icon and context menu in the Data Grid, enabled easier inspection of complex registry types like REG_BINARY, REG_EXPAND_SZ, and REG_MULTI_SZ, etc. Thanks [@htcfreek](https://github.com/htcfreek)!
|
|
||||||
- Improved file-saving experience in Registry Preview by aligning with Notepad-like behavior, enhancing user prompts, error handling, and preventing crashes during unsaved or interrupted actions. Thanks [@htcfreek](https://github.com/htcfreek)!
|
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
- Added an option to hide or show the PowerToys system tray icon. Thanks [@BLM16](https://github.com/BLM16)!
|
- Completely redesigned the Settings dashboard with a modern card-based layout featuring organized sections for quick actions and shortcuts overview, replacing the old module list.
|
||||||
- Improved settings to show progress while a bug report package is being generated.
|
- Rewrote setting descriptions to be more concise and follow Windows writing style guidelines, making them easier to understand.
|
||||||
|
- Improved formatting and readability of release notes in the "What's New" section with better typography and spacing.
|
||||||
### Workspaces
|
- Added missing deep link support for various settings pages (Peek, Quick Accent, PowerToys Run, etc.) so you can jump directly to specific settings.
|
||||||
|
- Resolved an issue where the settings page header would drift away from its position when resizing the settings window.
|
||||||
- Stored Workspaces icons in user AppData to ensure profile portability and prevent loss during temporary folder cleanup.
|
- Resolved a settings crash related to incompatible property names in ZoomIt configuration.
|
||||||
- Enabled capture and launch of PWAs on non-default Edge or Chrome profiles, ensuring consistent behavior during creation and execution.
|
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- Added SpeedTest and Dictionary Definition to the third-party plugins documentation for PowerToys Run. Thanks [@ruslanlap](https://github.com/ruslanlap)!
|
- Added detailed step-by-step instructions for first-time developers building the Command Palette module, including prerequisites and Visual Studio setup guidance. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||||
- Corrected sample links and typo in Command Palette documentation. Thanks [@daverayment](https://github.com/daverayment) and [@roycewilliams](https://github.com/roycewilliams)!
|
- **Fixed Broken SDK Link**: Corrected a broken markdown link in the Command Palette SDK README that was pointing to an incorrect directory path. Thanks [@ChrisGuzak](https://github.com/ChrisGuzak)!
|
||||||
|
- Added documentation for the "Open With Cursor" plugin that enables opening Visual Studio and VS Code recent files using Cursor AI. Thanks [@VictorNoxx](https://github.com/VictorNoxx)!
|
||||||
|
- Added documentation for two new community plugins - Hotkeys plugin for creating custom keyboard shortcuts, and RandomGen plugin for generating random data like passwords, colors, and placeholder text. Thanks [@ruslanlap](https://github.com/ruslanlap)!
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
- Updated .NET libraries to 9.0.6 for performance and security. Thanks [@snickler](https://github.com/snickler)!
|
- Updated .NET libraries to 9.0.8 for performance and security. Thanks [@snickler](https://github.com/snickler)!
|
||||||
- Updated WinAppSDK to 1.7.2 for better stability and Windows support.
|
- Updated the spell check system to version 0.0.25 with better GitHub integration and SARIF reporting, plus fixed numerous spelling errors throughout the codebase including property names and documentation. Thanks [@jsoref](https://github.com/jsoref)!
|
||||||
- Introduced a one-step local build script that generates a signed installer, enhancing developer productivity.
|
- Cleaned up spelling check configuration to eliminate false positives and excessive noise that was appearing in every pull request, making the development process smoother.
|
||||||
- Generated portable PDBs so cross-platform debuggers can read symbol files, improving debugging experience in VSCode and other tools.
|
- Replaced NuGet feed with Azure Artifacts for better package management.
|
||||||
- Simplified WinGet configuration files by using the [Microsoft.Windows.Settings](https://www.powershellgallery.com/packages/Microsoft.Windows.Settings) module to enable Developer Mode. Thanks [@mdanish-kh](https://github.com/mdanish-kh)!
|
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours.
|
||||||
- Adjusted build scripts for the latest Az.Accounts module to keep CI green.
|
- Replaced brittle pixel-by-pixel image comparison with perceptual hash (pHash) technology that's more robust to minor rendering differences - no more test failures due to anti-aliasing or compression artifacts.
|
||||||
- Streamlined release pipeline by removing hard-coded telemetry version numbers, and unified Command Palette versioning with Windows Terminal's versioning method for consistent updates.
|
- Reduced CI/fuzzing/UI test timeouts from 4 hours to 90 minutes, dramatically improving developer feedback loops and preventing long waits when builds get stuck.
|
||||||
- Enhanced the build validation step to show detailed differences between NOTICE.md and actual package dependencies and versions.
|
- Standardized test project naming across the entire codebase and improved pipeline result identification by adding platform/install mode context to test run titles. Thanks [@khmyznikov](https://github.com/khmyznikov)!
|
||||||
- Improved spell-checking accuracy across the repo. Thanks [@rovercoder](https://github.com/rovercoder)!
|
- Added comprehensive UI test suites for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename - ensuring better reliability and quality.
|
||||||
- Upgraded CI to TouchdownBuild v5 for faster pipelines.
|
- Enhanced UI test automation with command-line argument support, better session management, and improved element location methods using pattern matching to avoid failures from minor differences in exact matches.
|
||||||
- Added context comments to *Resources.resw* to help translators.
|
|
||||||
- Expanded fuzz testing coverage to include FancyZones.
|
|
||||||
- Integrated all unit tests into the CI pipeline, increasing from ~3,000 to ~5,000 tests.
|
|
||||||
- Enabled daily UI test automation on the main branch, now covering over 370 UI tests for end-to-end validation.
|
|
||||||
- Newly added unit tests for WorkspacesLib to improve reliability and maintainability.
|
|
||||||
|
|
||||||
### General
|
### What is being planned over the next few releases
|
||||||
|
|
||||||
- Updated bug report compression library (cziplib 0.3.3) for faster and more reliable package creation. Thanks [@Chubercik](https://github.com/Chubercik)!
|
For [v0.94][github-next-release-work], we'll work on the items below:
|
||||||
- Included App Installer (“AppX Deployment Server”) event logs in bug reports for more thorough diagnostics.
|
|
||||||
|
|
||||||
### What is being planned for version 0.93
|
|
||||||
|
|
||||||
For [v0.93][github-next-release-work], we'll work on the items below:
|
|
||||||
|
|
||||||
- Continued Command Palette polish
|
- Continued Command Palette polish
|
||||||
- New UI automation tests
|
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
|
||||||
- Working on installer upgrades
|
- Working on upgrading the installer to WiX 5
|
||||||
- Working on shortcut conflict detection
|
- Working on shortcut conflict detection
|
||||||
|
- Working on setting search
|
||||||
- Upgrading Keyboard Manager's editor UI
|
- Upgrading Keyboard Manager's editor UI
|
||||||
|
- New UI automation tests
|
||||||
- Stability, bug fixes
|
- Stability, bug fixes
|
||||||
|
|
||||||
## PowerToys Community
|
## PowerToys Community
|
||||||
|
@ -71,6 +71,41 @@ When the user changes settings in the UI:
|
|||||||
3. The runner calls the `set_config` function on the appropriate module
|
3. The runner calls the `set_config` function on the appropriate module
|
||||||
4. The module parses the JSON and applies the new settings
|
4. The module parses the JSON and applies the new settings
|
||||||
|
|
||||||
|
# Shortcut Conflict Detection
|
||||||
|
|
||||||
|
Steps to enable conflict detection for a hotkey:
|
||||||
|
|
||||||
|
### 1. Implement module interface for hotkeys
|
||||||
|
Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional<HotkeyEx> GetHotkeyEx()`.
|
||||||
|
|
||||||
|
- If not yet implemented, you need to add it so that it returns all hotkeys used by the module.
|
||||||
|
- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup.
|
||||||
|
- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp`
|
||||||
|
|
||||||
|
### 2. Implement IHotkeyConfig in the module settings (UI side)
|
||||||
|
Make sure the module’s settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`.
|
||||||
|
|
||||||
|
- This method should return all hotkeys used in the module.
|
||||||
|
- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`).
|
||||||
|
- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs`
|
||||||
|
- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings.
|
||||||
|
It provides both `getter` and `setter` methods to read and update the corresponding hotkey.
|
||||||
|
Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey.
|
||||||
|
This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`
|
||||||
|
|
||||||
|
### 3. Update the module’s ViewModel
|
||||||
|
The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()`.
|
||||||
|
|
||||||
|
- This method should return all hotkeys, maintaining the same order as in steps 1 and 2.
|
||||||
|
- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs`
|
||||||
|
|
||||||
|
### 4. Ensure the module’s Views call `OnPageLoaded()`
|
||||||
|
Once the module’s view is loaded, make sure to invoke the ViewModel’s `OnPageLoaded()` method:
|
||||||
|
```cs
|
||||||
|
Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||||
|
```
|
||||||
|
- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
|
||||||
|
|
||||||
## Debugging Settings
|
## Debugging Settings
|
||||||
|
|
||||||
To debug settings issues:
|
To debug settings issues:
|
||||||
|
@ -52,7 +52,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
|
|||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- **Follow the pattern of what you already see in the code.**
|
- **Follow the pattern of what you already see in the code.**
|
||||||
- [Coding style](development/style.md).
|
- [Coding style](style.md).
|
||||||
- Try to package new functionality/components into libraries that have nicely defined interfaces.
|
- Try to package new functionality/components into libraries that have nicely defined interfaces.
|
||||||
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
|
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
|
||||||
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.
|
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<!-- Resource directories should be added only if the installer is built on the build farm -->
|
<!-- Resource directories should be added only if the installer is built on the build farm -->
|
||||||
<?ifdef env.IsPipeline?>
|
<?ifdef env.IsPipeline?>
|
||||||
<?foreach ParentDirectory in INSTALLFOLDER;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
|
<?foreach ParentDirectory in INSTALLFOLDER;WinUI3AppsInstallFolder;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
|
||||||
<DirectoryRef Id="$(var.ParentDirectory)">
|
<DirectoryRef Id="$(var.ParentDirectory)">
|
||||||
<!-- Resource file directories -->
|
<!-- Resource file directories -->
|
||||||
<?foreach Language in $(var.LocLanguageList)?>
|
<?foreach Language in $(var.LocLanguageList)?>
|
||||||
@ -181,7 +181,7 @@
|
|||||||
</Component>
|
</Component>
|
||||||
<Component
|
<Component
|
||||||
Id="ImageResizer_$(var.IdSafeLanguage)_Component"
|
Id="ImageResizer_$(var.IdSafeLanguage)_Component"
|
||||||
Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER"
|
Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder"
|
||||||
Guid="$(var.CompGUIDPrefix)02">
|
Guid="$(var.CompGUIDPrefix)02">
|
||||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||||
<RegistryValue Type="string" Name="ImageResizer_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes"/>
|
<RegistryValue Type="string" Name="ImageResizer_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes"/>
|
||||||
@ -553,6 +553,7 @@
|
|||||||
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)HistoryPluginFolder" Directory="Resource$(var.IdSafeLanguage)HistoryPluginFolder" On="uninstall"/>
|
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)HistoryPluginFolder" Directory="Resource$(var.IdSafeLanguage)HistoryPluginFolder" On="uninstall"/>
|
||||||
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)PowerToysPluginFolder" Directory="Resource$(var.IdSafeLanguage)PowerToysPluginFolder" On="uninstall"/>
|
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)PowerToysPluginFolder" Directory="Resource$(var.IdSafeLanguage)PowerToysPluginFolder" On="uninstall"/>
|
||||||
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" Directory="Resource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" On="uninstall"/>
|
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" Directory="Resource$(var.IdSafeLanguage)ValueGeneratorPluginFolder" On="uninstall"/>
|
||||||
|
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" On="uninstall"/>
|
||||||
<?undef IdSafeLanguage?>
|
<?undef IdSafeLanguage?>
|
||||||
<?endforeach?>
|
<?endforeach?>
|
||||||
</Component>
|
</Component>
|
||||||
|
@ -112,7 +112,7 @@ private:
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
|
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -122,6 +122,7 @@ private:
|
|||||||
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
||||||
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
||||||
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
||||||
|
hotkey.isShown = isShown;
|
||||||
return hotkey;
|
return hotkey;
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
@ -231,8 +232,10 @@ private:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue)
|
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true)
|
||||||
{
|
{
|
||||||
|
bool actionIsShown = true;
|
||||||
|
|
||||||
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
|
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -240,9 +243,9 @@ private:
|
|||||||
|
|
||||||
const auto action = actionValue.GetObjectW();
|
const auto action = actionValue.GetObjectW();
|
||||||
|
|
||||||
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
|
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown)
|
||||||
{
|
{
|
||||||
return;
|
actionIsShown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.HasKey(JSON_KEY_SHORTCUT))
|
if (action.HasKey(JSON_KEY_SHORTCUT))
|
||||||
@ -250,7 +253,7 @@ private:
|
|||||||
const AdditionalAction additionalAction
|
const AdditionalAction additionalAction
|
||||||
{
|
{
|
||||||
actionName.c_str(),
|
actionName.c_str(),
|
||||||
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
|
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
|
||||||
};
|
};
|
||||||
|
|
||||||
m_additional_actions.push_back(additionalAction);
|
m_additional_actions.push_back(additionalAction);
|
||||||
@ -259,12 +262,12 @@ private:
|
|||||||
{
|
{
|
||||||
for (const auto& [subActionName, subAction] : action)
|
for (const auto& [subActionName, subAction] : action)
|
||||||
{
|
{
|
||||||
process_additional_action(subActionName, subAction);
|
process_additional_action(subActionName, subAction, actionIsShown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
||||||
{
|
{
|
||||||
const auto settingsObject = settings.get_raw_json();
|
const auto settingsObject = settings.get_raw_json();
|
||||||
|
|
||||||
@ -317,9 +320,21 @@ private:
|
|||||||
{
|
{
|
||||||
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
|
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
|
||||||
|
|
||||||
for (const auto& [actionName, additionalAction] : additionalActions)
|
// Define the expected order to ensure consistent hotkey ID assignment
|
||||||
|
const std::vector<winrt::hstring> expectedOrder = {
|
||||||
|
L"image-to-text",
|
||||||
|
L"paste-as-file",
|
||||||
|
L"transcode"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process actions in the predefined order
|
||||||
|
for (auto& actionKey : expectedOrder)
|
||||||
{
|
{
|
||||||
process_additional_action(actionName, additionalAction);
|
if (additionalActions.HasKey(actionKey))
|
||||||
|
{
|
||||||
|
const auto actionValue = additionalActions.GetNamedValue(actionKey);
|
||||||
|
process_additional_action(actionKey, actionValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,17 +346,14 @@ private:
|
|||||||
for (const auto& customAction : customActions)
|
for (const auto& customAction : customActions)
|
||||||
{
|
{
|
||||||
const auto object = customAction.GetObjectW();
|
const auto object = customAction.GetObjectW();
|
||||||
|
bool actionIsShown = object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false);
|
||||||
|
|
||||||
if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
|
const CustomAction customActionData{
|
||||||
{
|
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
|
||||||
const CustomAction customActionData
|
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
|
||||||
{
|
};
|
||||||
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
|
|
||||||
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
|
|
||||||
};
|
|
||||||
|
|
||||||
m_custom_actions.push_back(customActionData);
|
m_custom_actions.push_back(customActionData);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,5 +298,34 @@ namespace Hosts.Tests
|
|||||||
var hidden = fileSystem.FileInfo.New(service.HostsFilePath).Attributes.HasFlag(FileAttributes.Hidden);
|
var hidden = fileSystem.FileInfo.New(service.HostsFilePath).Attributes.HasFlag(FileAttributes.Hidden);
|
||||||
Assert.IsTrue(hidden);
|
Assert.IsTrue(hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task NoLeadingSpaces_Disabled_RemovesIndent()
|
||||||
|
{
|
||||||
|
var content =
|
||||||
|
@"10.1.1.1 host host.local # comment
|
||||||
|
10.1.1.2 host2 host2.local # another comment
|
||||||
|
";
|
||||||
|
|
||||||
|
var expected =
|
||||||
|
@"10.1.1.1 host host.local # comment
|
||||||
|
10.1.1.2 host2 host2.local # another comment
|
||||||
|
# 10.1.1.30 host30 host30.local # new entry
|
||||||
|
";
|
||||||
|
|
||||||
|
var fs = new CustomMockFileSystem();
|
||||||
|
var settings = new Mock<IUserSettings>();
|
||||||
|
settings.Setup(s => s.NoLeadingSpaces).Returns(true);
|
||||||
|
var svc = new HostsService(fs, settings.Object, _elevationHelper.Object);
|
||||||
|
fs.AddFile(svc.HostsFilePath, new MockFileData(content));
|
||||||
|
|
||||||
|
var data = await svc.ReadAsync();
|
||||||
|
var entries = data.Entries.ToList();
|
||||||
|
entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false));
|
||||||
|
await svc.WriteAsync(data.AdditionalLines, entries);
|
||||||
|
|
||||||
|
var result = fs.GetFile(svc.HostsFilePath);
|
||||||
|
Assert.AreEqual(expected, result.TextContents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ namespace Hosts.Settings
|
|||||||
|
|
||||||
private bool _loopbackDuplicates;
|
private bool _loopbackDuplicates;
|
||||||
|
|
||||||
|
public bool NoLeadingSpaces { get; private set; }
|
||||||
|
|
||||||
public bool LoopbackDuplicates
|
public bool LoopbackDuplicates
|
||||||
{
|
{
|
||||||
get => _loopbackDuplicates;
|
get => _loopbackDuplicates;
|
||||||
@ -88,6 +90,7 @@ namespace Hosts.Settings
|
|||||||
AdditionalLinesPosition = (HostsAdditionalLinesPosition)settings.Properties.AdditionalLinesPosition;
|
AdditionalLinesPosition = (HostsAdditionalLinesPosition)settings.Properties.AdditionalLinesPosition;
|
||||||
Encoding = (HostsEncoding)settings.Properties.Encoding;
|
Encoding = (HostsEncoding)settings.Properties.Encoding;
|
||||||
LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
|
LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
|
||||||
|
NoLeadingSpaces = settings.Properties.NoLeadingSpaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
retry = false;
|
retry = false;
|
||||||
|
@ -157,7 +157,7 @@ namespace HostsUILib.Helpers
|
|||||||
{
|
{
|
||||||
lineBuilder.Append('#').Append(' ');
|
lineBuilder.Append('#').Append(' ');
|
||||||
}
|
}
|
||||||
else if (anyDisabled)
|
else if (anyDisabled && !_userSettings.NoLeadingSpaces)
|
||||||
{
|
{
|
||||||
lineBuilder.Append(' ').Append(' ');
|
lineBuilder.Append(' ').Append(' ');
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,7 @@ namespace HostsUILib.Settings
|
|||||||
event EventHandler LoopbackDuplicatesChanged;
|
event EventHandler LoopbackDuplicatesChanged;
|
||||||
|
|
||||||
public delegate void OpenSettingsFunction();
|
public delegate void OpenSettingsFunction();
|
||||||
|
|
||||||
|
public bool NoLeadingSpaces { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
#include "MouseHighlighter.h"
|
#include "MouseHighlighter.h"
|
||||||
#include "trace.h"
|
#include "trace.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#ifdef COMPOSITION
|
#ifdef COMPOSITION
|
||||||
namespace winrt
|
namespace winrt
|
||||||
@ -49,6 +50,9 @@ private:
|
|||||||
void BringToFront();
|
void BringToFront();
|
||||||
HHOOK m_mouseHook = NULL;
|
HHOOK m_mouseHook = NULL;
|
||||||
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
|
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
|
||||||
|
// Helpers for spotlight overlay
|
||||||
|
float GetDpiScale() const;
|
||||||
|
void UpdateSpotlightMask(float cx, float cy, float radius, bool show);
|
||||||
|
|
||||||
static constexpr auto m_className = L"MouseHighlighter";
|
static constexpr auto m_className = L"MouseHighlighter";
|
||||||
static constexpr auto m_windowTitle = L"PowerToys Mouse Highlighter";
|
static constexpr auto m_windowTitle = L"PowerToys Mouse Highlighter";
|
||||||
@ -67,7 +71,14 @@ private:
|
|||||||
winrt::CompositionSpriteShape m_leftPointer{ nullptr };
|
winrt::CompositionSpriteShape m_leftPointer{ nullptr };
|
||||||
winrt::CompositionSpriteShape m_rightPointer{ nullptr };
|
winrt::CompositionSpriteShape m_rightPointer{ nullptr };
|
||||||
winrt::CompositionSpriteShape m_alwaysPointer{ nullptr };
|
winrt::CompositionSpriteShape m_alwaysPointer{ nullptr };
|
||||||
winrt::CompositionSpriteShape m_spotlightPointer{ nullptr };
|
// Spotlight overlay (mask with soft feathered edge)
|
||||||
|
winrt::SpriteVisual m_overlay{ nullptr };
|
||||||
|
winrt::CompositionMaskBrush m_spotlightMask{ nullptr };
|
||||||
|
winrt::CompositionRadialGradientBrush m_spotlightMaskGradient{ nullptr };
|
||||||
|
winrt::CompositionColorBrush m_spotlightSource{ nullptr };
|
||||||
|
winrt::CompositionColorGradientStop m_maskStopCenter{ nullptr };
|
||||||
|
winrt::CompositionColorGradientStop m_maskStopInner{ nullptr };
|
||||||
|
winrt::CompositionColorGradientStop m_maskStopOuter{ nullptr };
|
||||||
|
|
||||||
bool m_leftPointerEnabled = true;
|
bool m_leftPointerEnabled = true;
|
||||||
bool m_rightPointerEnabled = true;
|
bool m_rightPointerEnabled = true;
|
||||||
@ -123,6 +134,35 @@ bool Highlighter::CreateHighlighter()
|
|||||||
m_shape.RelativeSizeAdjustment({ 1.0f, 1.0f });
|
m_shape.RelativeSizeAdjustment({ 1.0f, 1.0f });
|
||||||
m_root.Children().InsertAtTop(m_shape);
|
m_root.Children().InsertAtTop(m_shape);
|
||||||
|
|
||||||
|
// Create spotlight overlay (soft feather, DPI-aware)
|
||||||
|
m_overlay = m_compositor.CreateSpriteVisual();
|
||||||
|
m_overlay.RelativeSizeAdjustment({ 1.0f, 1.0f });
|
||||||
|
m_spotlightSource = m_compositor.CreateColorBrush(m_alwaysColor);
|
||||||
|
m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush();
|
||||||
|
m_spotlightMaskGradient.MappingMode(winrt::CompositionMappingMode::Absolute);
|
||||||
|
// Center region fully transparent
|
||||||
|
m_maskStopCenter = m_compositor.CreateColorGradientStop();
|
||||||
|
m_maskStopCenter.Offset(0.0f);
|
||||||
|
m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
|
||||||
|
// Inner edge of feather (still transparent)
|
||||||
|
m_maskStopInner = m_compositor.CreateColorGradientStop();
|
||||||
|
m_maskStopInner.Offset(0.995f); // will be updated per-radius
|
||||||
|
m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
|
||||||
|
// Outer edge (opaque mask -> overlay visible)
|
||||||
|
m_maskStopOuter = m_compositor.CreateColorGradientStop();
|
||||||
|
m_maskStopOuter.Offset(1.0f);
|
||||||
|
m_maskStopOuter.Color(winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255));
|
||||||
|
m_spotlightMaskGradient.ColorStops().Append(m_maskStopCenter);
|
||||||
|
m_spotlightMaskGradient.ColorStops().Append(m_maskStopInner);
|
||||||
|
m_spotlightMaskGradient.ColorStops().Append(m_maskStopOuter);
|
||||||
|
|
||||||
|
m_spotlightMask = m_compositor.CreateMaskBrush();
|
||||||
|
m_spotlightMask.Source(m_spotlightSource);
|
||||||
|
m_spotlightMask.Mask(m_spotlightMaskGradient);
|
||||||
|
m_overlay.Brush(m_spotlightMask);
|
||||||
|
m_overlay.IsVisible(false);
|
||||||
|
m_root.Children().InsertAtTop(m_overlay);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
@ -165,12 +205,8 @@ void Highlighter::AddDrawingPoint(MouseButton button)
|
|||||||
// always
|
// always
|
||||||
if (m_spotlightMode)
|
if (m_spotlightMode)
|
||||||
{
|
{
|
||||||
float borderThickness = static_cast<float>(std::hypot(GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN)));
|
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
|
||||||
circleGeometry.Radius({ static_cast<float>(borderThickness / 2.0 + m_radius), static_cast<float>(borderThickness / 2.0 + m_radius) });
|
return;
|
||||||
circleShape.FillBrush(nullptr);
|
|
||||||
circleShape.StrokeBrush(m_compositor.CreateColorBrush(m_alwaysColor));
|
|
||||||
circleShape.StrokeThickness(borderThickness);
|
|
||||||
m_spotlightPointer = circleShape;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -209,20 +245,14 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// always
|
// always / spotlight idle
|
||||||
if (m_spotlightMode)
|
if (m_spotlightMode)
|
||||||
{
|
{
|
||||||
if (m_spotlightPointer)
|
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
|
||||||
{
|
|
||||||
m_spotlightPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else if (m_alwaysPointer)
|
||||||
{
|
{
|
||||||
if (m_alwaysPointer)
|
m_alwaysPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
||||||
{
|
|
||||||
m_alwaysPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,9 +296,9 @@ void Highlighter::ClearDrawingPoint()
|
|||||||
{
|
{
|
||||||
if (m_spotlightMode)
|
if (m_spotlightMode)
|
||||||
{
|
{
|
||||||
if (m_spotlightPointer)
|
if (m_overlay)
|
||||||
{
|
{
|
||||||
m_spotlightPointer.StrokeBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
|
m_overlay.IsVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -421,7 +451,10 @@ void Highlighter::StopDrawing()
|
|||||||
m_leftPointer = nullptr;
|
m_leftPointer = nullptr;
|
||||||
m_rightPointer = nullptr;
|
m_rightPointer = nullptr;
|
||||||
m_alwaysPointer = nullptr;
|
m_alwaysPointer = nullptr;
|
||||||
m_spotlightPointer = nullptr;
|
if (m_overlay)
|
||||||
|
{
|
||||||
|
m_overlay.IsVisible(false);
|
||||||
|
}
|
||||||
ShowWindow(m_hwnd, SW_HIDE);
|
ShowWindow(m_hwnd, SW_HIDE);
|
||||||
UnhookWindowsHookEx(m_mouseHook);
|
UnhookWindowsHookEx(m_mouseHook);
|
||||||
ClearDrawing();
|
ClearDrawing();
|
||||||
@ -452,6 +485,16 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings)
|
|||||||
m_rightPointerEnabled = false;
|
m_rightPointerEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep spotlight overlay color updated
|
||||||
|
if (m_spotlightSource)
|
||||||
|
{
|
||||||
|
m_spotlightSource.Color(m_alwaysColor);
|
||||||
|
}
|
||||||
|
if (!m_spotlightMode && m_overlay)
|
||||||
|
{
|
||||||
|
m_overlay.IsVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (instance->m_visible)
|
if (instance->m_visible)
|
||||||
{
|
{
|
||||||
instance->StopDrawing();
|
instance->StopDrawing();
|
||||||
@ -563,6 +606,43 @@ void Highlighter::Terminate()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float Highlighter::GetDpiScale() const
|
||||||
|
{
|
||||||
|
return static_cast<float>(GetDpiForWindow(m_hwnd)) / 96.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update spotlight radial mask center/radius with DPI-aware feather
|
||||||
|
void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool show)
|
||||||
|
{
|
||||||
|
if (!m_spotlightMaskGradient)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_spotlightMaskGradient.EllipseCenter({ cx, cy });
|
||||||
|
m_spotlightMaskGradient.EllipseRadius({ radius, radius });
|
||||||
|
|
||||||
|
const float dpiScale = GetDpiScale();
|
||||||
|
// Target a very fine edge: ~1 physical pixel, convert to DIPs: 1 / dpiScale
|
||||||
|
const float featherDip = 1.0f / (dpiScale > 0.0f ? dpiScale : 1.0f);
|
||||||
|
const float safeRadius = (std::max)(radius, 1.0f);
|
||||||
|
const float featherRel = (std::min)(0.25f, featherDip / safeRadius);
|
||||||
|
|
||||||
|
if (m_maskStopInner)
|
||||||
|
{
|
||||||
|
m_maskStopInner.Offset((std::max)(0.0f, 1.0f - featherRel));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_spotlightSource)
|
||||||
|
{
|
||||||
|
m_spotlightSource.Color(m_alwaysColor);
|
||||||
|
}
|
||||||
|
if (m_overlay)
|
||||||
|
{
|
||||||
|
m_overlay.IsVisible(show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma region MouseHighlighter_API
|
#pragma region MouseHighlighter_API
|
||||||
|
|
||||||
void MouseHighlighterApplySettings(MouseHighlighterSettings settings)
|
void MouseHighlighterApplySettings(MouseHighlighterSettings settings)
|
||||||
|
@ -556,6 +556,61 @@ public:
|
|||||||
return m_enabled;
|
return m_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
||||||
|
{
|
||||||
|
constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
|
||||||
|
|
||||||
|
if (hotkeys && buffer_size >= num_hotkeys)
|
||||||
|
{
|
||||||
|
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
|
||||||
|
|
||||||
|
// Cache the raw JSON object to avoid multiple parsing
|
||||||
|
json::JsonObject root_json = values.get_raw_json();
|
||||||
|
json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
|
||||||
|
|
||||||
|
size_t hotkey_index = 0;
|
||||||
|
|
||||||
|
// Helper lambda to extract hotkey from JSON properties
|
||||||
|
auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey {
|
||||||
|
if (properties_json.HasKey(property_name))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
|
||||||
|
|
||||||
|
// Extract hotkey properties directly from JSON
|
||||||
|
bool win = hotkey_json.GetNamedBoolean(L"win", false);
|
||||||
|
bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false);
|
||||||
|
bool alt = hotkey_json.GetNamedBoolean(L"alt", false);
|
||||||
|
bool shift = hotkey_json.GetNamedBoolean(L"shift", false);
|
||||||
|
unsigned char key = static_cast<unsigned char>(
|
||||||
|
hotkey_json.GetNamedNumber(L"code", 0));
|
||||||
|
|
||||||
|
return { win, ctrl, shift, alt, key };
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
// If parsing individual hotkey fails, use defaults
|
||||||
|
return { false, false, false, false, 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Property doesn't exist, use defaults
|
||||||
|
return { false, false, false, false, 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract all hotkeys using the optimized helper
|
||||||
|
hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse
|
||||||
|
hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine
|
||||||
|
hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs
|
||||||
|
hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect
|
||||||
|
}
|
||||||
|
|
||||||
|
return num_hotkeys;
|
||||||
|
}
|
||||||
|
|
||||||
void launch_add_firewall_process()
|
void launch_add_firewall_process()
|
||||||
{
|
{
|
||||||
Logger::trace(L"Starting Process to add firewall rule");
|
Logger::trace(L"Starting Process to add firewall rule");
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
// 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.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Common.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides utility methods for building diagnostic and error messages.
|
||||||
|
/// </summary>
|
||||||
|
public static class DiagnosticsHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a comprehensive exception message with timestamp and detailed diagnostic information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">The exception that occurred.</param>
|
||||||
|
/// <param name="extensionHint">A hint about which extension caused the exception to help with debugging.</param>
|
||||||
|
/// <returns>A string containing the exception details, timestamp, and source information for diagnostic purposes.</returns>
|
||||||
|
public static string BuildExceptionMessage(Exception exception, string? extensionHint)
|
||||||
|
{
|
||||||
|
var locationHint = string.IsNullOrWhiteSpace(extensionHint) ? "application" : $"'{extensionHint}' extension";
|
||||||
|
|
||||||
|
// let's try to get a message from the exception or inferred it from the HRESULT
|
||||||
|
// to show at least something
|
||||||
|
var message = exception.Message;
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
var temp = Marshal.GetExceptionForHR(exception.HResult)?.Message;
|
||||||
|
if (!string.IsNullOrWhiteSpace(temp))
|
||||||
|
{
|
||||||
|
message = temp + $" (inferred from HRESULT 0x{exception.HResult:X8})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
message = "[No message available]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// note: keep date time kind and format consistent with the log
|
||||||
|
return $"""
|
||||||
|
============================================================
|
||||||
|
😢 An unexpected error occurred in the {locationHint}.
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Message: {message}
|
||||||
|
Type: {exception.GetType().FullName}
|
||||||
|
Source: {exception.Source ?? "N/A"}
|
||||||
|
Time: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fffffff}
|
||||||
|
HRESULT: 0x{exception.HResult:X8} ({exception.HResult})
|
||||||
|
|
||||||
|
Stack Trace:
|
||||||
|
{exception.StackTrace ?? "[No stack trace available]"}
|
||||||
|
|
||||||
|
------------------ Full Exception Details ------------------
|
||||||
|
{exception}
|
||||||
|
|
||||||
|
ℹ️ If you need further assistance, please include this information in your support request.
|
||||||
|
ℹ️ Before sending, take a quick look to make sure it doesn't contain any personal or sensitive information.
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,7 @@ public partial class ExtensionHostInstance
|
|||||||
/// <param name="message">The log message to send</param>
|
/// <param name="message">The log message to send</param>
|
||||||
public void LogMessage(ILogMessage message)
|
public void LogMessage(ILogMessage message)
|
||||||
{
|
{
|
||||||
if (Host != null)
|
if (Host is not null)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@ -47,7 +47,7 @@ public partial class ExtensionHostInstance
|
|||||||
|
|
||||||
public void ShowStatus(IStatusMessage message, StatusContext context)
|
public void ShowStatus(IStatusMessage message, StatusContext context)
|
||||||
{
|
{
|
||||||
if (Host != null)
|
if (Host is not null)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@ -64,7 +64,7 @@ public partial class ExtensionHostInstance
|
|||||||
|
|
||||||
public void HideStatus(IStatusMessage message)
|
public void HideStatus(IStatusMessage message)
|
||||||
{
|
{
|
||||||
if (Host != null)
|
if (Host is not null)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
@ -89,7 +89,7 @@ public class SupersedingAsyncGate : IDisposable
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
CompleteIfCurrent(currentTcs, currentCallId, tcs => tcs.SetCanceled(currentCts.Token));
|
CompleteIfCurrent(currentTcs, currentCallId, tcs => tcs.TrySetCanceled(currentCts.Token));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -36,7 +36,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
|||||||
|
|
||||||
public IAsyncAction HideStatus(IStatusMessage? message)
|
public IAsyncAction HideStatus(IStatusMessage? message)
|
||||||
{
|
{
|
||||||
if (message == null)
|
if (message is null)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask.AsAsyncAction();
|
return Task.CompletedTask.AsAsyncAction();
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
|||||||
|
|
||||||
public IAsyncAction LogMessage(ILogMessage? message)
|
public IAsyncAction LogMessage(ILogMessage? message)
|
||||||
{
|
{
|
||||||
if (message == null)
|
if (message is null)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask.AsAsyncAction();
|
return Task.CompletedTask.AsAsyncAction();
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var vm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
|
var vm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
|
||||||
if (vm != null)
|
if (vm is not null)
|
||||||
{
|
{
|
||||||
StatusMessages.Remove(vm);
|
StatusMessages.Remove(vm);
|
||||||
}
|
}
|
||||||
@ -113,7 +113,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
|||||||
{
|
{
|
||||||
// If this message is already in the list of messages, just bring it to the top
|
// If this message is already in the list of messages, just bring it to the top
|
||||||
var oldVm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
|
var oldVm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
|
||||||
if (oldVm != null)
|
if (oldVm is not null)
|
||||||
{
|
{
|
||||||
Task.Factory.StartNew(
|
Task.Factory.StartNew(
|
||||||
() =>
|
() =>
|
||||||
@ -142,7 +142,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
|||||||
|
|
||||||
public IAsyncAction ShowStatus(IStatusMessage? message, StatusContext context)
|
public IAsyncAction ShowStatus(IStatusMessage? message, StatusContext context)
|
||||||
{
|
{
|
||||||
if (message == null)
|
if (message is null)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask.AsAsyncAction();
|
return Task.CompletedTask.AsAsyncAction();
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
@ -35,13 +34,13 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
[NotifyPropertyChangedFor(nameof(HasPrimaryCommand))]
|
[NotifyPropertyChangedFor(nameof(HasPrimaryCommand))]
|
||||||
public partial CommandItemViewModel? PrimaryCommand { get; set; }
|
public partial CommandItemViewModel? PrimaryCommand { get; set; }
|
||||||
|
|
||||||
public bool HasPrimaryCommand => PrimaryCommand != null && PrimaryCommand.ShouldBeVisible;
|
public bool HasPrimaryCommand => PrimaryCommand is not null && PrimaryCommand.ShouldBeVisible;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(HasSecondaryCommand))]
|
[NotifyPropertyChangedFor(nameof(HasSecondaryCommand))]
|
||||||
public partial CommandItemViewModel? SecondaryCommand { get; set; }
|
public partial CommandItemViewModel? SecondaryCommand { get; set; }
|
||||||
|
|
||||||
public bool HasSecondaryCommand => SecondaryCommand != null;
|
public bool HasSecondaryCommand => SecondaryCommand is not null;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool ShouldShowContextMenu { get; set; } = false;
|
public partial bool ShouldShowContextMenu { get; set; } = false;
|
||||||
@ -58,14 +57,14 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
|
|
||||||
private void SetSelectedItem(ICommandBarContext? value)
|
private void SetSelectedItem(ICommandBarContext? value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value is not null)
|
||||||
{
|
{
|
||||||
PrimaryCommand = value.PrimaryCommand;
|
PrimaryCommand = value.PrimaryCommand;
|
||||||
value.PropertyChanged += SelectedItemPropertyChanged;
|
value.PropertyChanged += SelectedItemPropertyChanged;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (SelectedItem != null)
|
if (SelectedItem is not null)
|
||||||
{
|
{
|
||||||
SelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
|
SelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
|
||||||
}
|
}
|
||||||
@ -88,7 +87,7 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
|
|
||||||
private void UpdateContextItems()
|
private void UpdateContextItems()
|
||||||
{
|
{
|
||||||
if (SelectedItem == null)
|
if (SelectedItem is null)
|
||||||
{
|
{
|
||||||
SecondaryCommand = null;
|
SecondaryCommand = null;
|
||||||
ShouldShowContextMenu = false;
|
ShouldShowContextMenu = false;
|
||||||
@ -127,13 +126,13 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||||
{
|
{
|
||||||
var keybindings = SelectedItem?.Keybindings();
|
var keybindings = SelectedItem?.Keybindings();
|
||||||
if (keybindings != null)
|
if (keybindings is not null)
|
||||||
{
|
{
|
||||||
// Does the pressed key match any of the keybindings?
|
// Does the pressed key match any of the keybindings?
|
||||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
||||||
if (keybindings.TryGetValue(pressedKeyChord, out var matchedItem))
|
if (keybindings.TryGetValue(pressedKeyChord, out var matchedItem))
|
||||||
{
|
{
|
||||||
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
|
return matchedItem is not null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +141,7 @@ public partial class CommandBarViewModel : ObservableObject,
|
|||||||
|
|
||||||
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
|
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
|
||||||
{
|
{
|
||||||
if (command == null)
|
if (command is null)
|
||||||
{
|
{
|
||||||
return ContextKeybindingResult.Unhandled;
|
return ContextKeybindingResult.Unhandled;
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||||
|
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
|
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
|
||||||
{
|
{
|
||||||
private readonly KeyChord nullKeyChord = new(0, 0, 0);
|
private readonly KeyChord nullKeyChord = new(0, 0, 0);
|
||||||
@ -17,7 +20,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
|
|||||||
|
|
||||||
public KeyChord? RequestedShortcut { get; private set; }
|
public KeyChord? RequestedShortcut { get; private set; }
|
||||||
|
|
||||||
public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord);
|
public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord);
|
||||||
|
|
||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
@ -29,7 +32,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
|
|||||||
base.InitializeProperties();
|
base.InitializeProperties();
|
||||||
|
|
||||||
var contextItem = Model.Unsafe;
|
var contextItem = Model.Unsafe;
|
||||||
if (contextItem == null)
|
if (contextItem is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
@ -9,6 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
|||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||||
|
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||||
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
|
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
|
||||||
{
|
{
|
||||||
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
||||||
@ -66,7 +68,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
List<IContextItemViewModel> l = _defaultCommandContextItem == null ?
|
List<IContextItemViewModel> l = _defaultCommandContextItem is null ?
|
||||||
new() :
|
new() :
|
||||||
[_defaultCommandContextItem];
|
[_defaultCommandContextItem];
|
||||||
|
|
||||||
@ -98,7 +100,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
var model = _commandItemModel.Unsafe;
|
var model = _commandItemModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -126,7 +128,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
var model = _commandItemModel.Unsafe;
|
var model = _commandItemModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -134,7 +136,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
Command.InitializeProperties();
|
Command.InitializeProperties();
|
||||||
|
|
||||||
var listIcon = model.Icon;
|
var listIcon = model.Icon;
|
||||||
if (listIcon != null)
|
if (listIcon is not null)
|
||||||
{
|
{
|
||||||
_listItemIcon = new(listIcon);
|
_listItemIcon = new(listIcon);
|
||||||
_listItemIcon.InitializeProperties();
|
_listItemIcon.InitializeProperties();
|
||||||
@ -170,13 +172,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
var model = _commandItemModel.Unsafe;
|
var model = _commandItemModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var more = model.MoreCommands;
|
var more = model.MoreCommands;
|
||||||
if (more != null)
|
if (more is not null)
|
||||||
{
|
{
|
||||||
MoreCommands = more
|
MoreCommands = more
|
||||||
.Select(item =>
|
.Select(item =>
|
||||||
@ -298,7 +300,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
protected virtual void FetchProperty(string propertyName)
|
protected virtual void FetchProperty(string propertyName)
|
||||||
{
|
{
|
||||||
var model = this._commandItemModel.Unsafe;
|
var model = this._commandItemModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -306,7 +308,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
switch (propertyName)
|
switch (propertyName)
|
||||||
{
|
{
|
||||||
case nameof(Command):
|
case nameof(Command):
|
||||||
if (Command != null)
|
if (Command is not null)
|
||||||
{
|
{
|
||||||
Command.PropertyChanged -= Command_PropertyChanged;
|
Command.PropertyChanged -= Command_PropertyChanged;
|
||||||
}
|
}
|
||||||
@ -337,7 +339,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
|
|
||||||
case nameof(model.MoreCommands):
|
case nameof(model.MoreCommands):
|
||||||
var more = model.MoreCommands;
|
var more = model.MoreCommands;
|
||||||
if (more != null)
|
if (more is not null)
|
||||||
{
|
{
|
||||||
var newContextMenu = more
|
var newContextMenu = more
|
||||||
.Select(item =>
|
.Select(item =>
|
||||||
@ -392,7 +394,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
||||||
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
||||||
var model = _commandItemModel.Unsafe;
|
var model = _commandItemModel.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
_itemTitle = model.Title;
|
_itemTitle = model.Title;
|
||||||
}
|
}
|
||||||
@ -428,7 +430,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
Command.SafeCleanup();
|
Command.SafeCleanup();
|
||||||
|
|
||||||
var model = _commandItemModel.Unsafe;
|
var model = _commandItemModel.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
model.PropChanged -= Model_PropChanged;
|
model.PropChanged -= Model_PropChanged;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -67,13 +67,13 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ico = model.Icon;
|
var ico = model.Icon;
|
||||||
if (ico != null)
|
if (ico is not null)
|
||||||
{
|
{
|
||||||
Icon = new(ico);
|
Icon = new(ico);
|
||||||
Icon.InitializeProperties();
|
Icon.InitializeProperties();
|
||||||
@ -98,7 +98,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
|||||||
protected void FetchProperty(string propertyName)
|
protected void FetchProperty(string propertyName)
|
||||||
{
|
{
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -125,7 +125,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
|||||||
Icon = new(null); // necessary?
|
Icon = new(null); // necessary?
|
||||||
|
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
model.PropChanged -= Model_PropChanged;
|
model.PropChanged -= Model_PropChanged;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ public partial class ConfirmResultViewModel(IConfirmationArgs _args, WeakReferen
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
public DetailsViewModel? Details { get; private set; }
|
public DetailsViewModel? Details { get; private set; }
|
||||||
|
|
||||||
[MemberNotNullWhen(true, nameof(Details))]
|
[MemberNotNullWhen(true, nameof(Details))]
|
||||||
public bool HasDetails => Details != null;
|
public bool HasDetails => Details is not null;
|
||||||
|
|
||||||
/////// ICommandBarContext ///////
|
/////// ICommandBarContext ///////
|
||||||
public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
|
public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
|
||||||
@ -67,7 +67,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
foreach (var item in newItems)
|
foreach (var item in newItems)
|
||||||
{
|
{
|
||||||
var viewModel = ViewModelFromContent(item, PageContext);
|
var viewModel = ViewModelFromContent(item, PageContext);
|
||||||
if (viewModel != null)
|
if (viewModel is not null)
|
||||||
{
|
{
|
||||||
viewModel.InitializeProperties();
|
viewModel.InitializeProperties();
|
||||||
newContent.Add(viewModel);
|
newContent.Add(viewModel);
|
||||||
@ -104,7 +104,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
base.InitializeProperties();
|
base.InitializeProperties();
|
||||||
|
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
});
|
});
|
||||||
|
|
||||||
var extensionDetails = model.Details;
|
var extensionDetails = model.Details;
|
||||||
if (extensionDetails != null)
|
if (extensionDetails is not null)
|
||||||
{
|
{
|
||||||
Details = new(extensionDetails, PageContext);
|
Details = new(extensionDetails, PageContext);
|
||||||
Details.InitializeProperties();
|
Details.InitializeProperties();
|
||||||
@ -156,7 +156,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
base.FetchProperty(propertyName);
|
base.FetchProperty(propertyName);
|
||||||
|
|
||||||
var model = this._model.Unsafe;
|
var model = this._model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
case nameof(Commands):
|
case nameof(Commands):
|
||||||
|
|
||||||
var more = model.Commands;
|
var more = model.Commands;
|
||||||
if (more != null)
|
if (more is not null)
|
||||||
{
|
{
|
||||||
var newContextMenu = more
|
var newContextMenu = more
|
||||||
.ToList()
|
.ToList()
|
||||||
@ -216,7 +216,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
break;
|
break;
|
||||||
case nameof(Details):
|
case nameof(Details):
|
||||||
var extensionDetails = model.Details;
|
var extensionDetails = model.Details;
|
||||||
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
|
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||||
UpdateDetails();
|
UpdateDetails();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -248,7 +248,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void InvokePrimaryCommand(ContentPageViewModel page)
|
private void InvokePrimaryCommand(ContentPageViewModel page)
|
||||||
{
|
{
|
||||||
if (PrimaryCommand != null)
|
if (PrimaryCommand is not null)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
|
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
|
||||||
}
|
}
|
||||||
@ -258,7 +258,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void InvokeSecondaryCommand(ContentPageViewModel page)
|
private void InvokeSecondaryCommand(ContentPageViewModel page)
|
||||||
{
|
{
|
||||||
if (SecondaryCommand != null)
|
if (SecondaryCommand is not null)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
|
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
|
||||||
}
|
}
|
||||||
@ -285,7 +285,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
|||||||
Content.Clear();
|
Content.Clear();
|
||||||
|
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
model.ItemsChanged -= Model_ItemsChanged;
|
model.ItemsChanged -= Model_ItemsChanged;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
using Microsoft.Diagnostics.Utilities;
|
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||||
@ -51,7 +50,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
|
|
||||||
public void UpdateContextItems()
|
public void UpdateContextItems()
|
||||||
{
|
{
|
||||||
if (SelectedItem != null)
|
if (SelectedItem is not null)
|
||||||
{
|
{
|
||||||
if (SelectedItem.MoreCommands.Count() > 1)
|
if (SelectedItem.MoreCommands.Count() > 1)
|
||||||
{
|
{
|
||||||
@ -68,14 +67,14 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SelectedItem == null)
|
if (SelectedItem is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_lastSearchText = searchText;
|
_lastSearchText = searchText;
|
||||||
|
|
||||||
if (CurrentContextMenu == null)
|
if (CurrentContextMenu is null)
|
||||||
{
|
{
|
||||||
ListHelpers.InPlaceUpdateList(FilteredItems, []);
|
ListHelpers.InPlaceUpdateList(FilteredItems, []);
|
||||||
return;
|
return;
|
||||||
@ -124,7 +123,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
/// that have a shortcut key set.</returns>
|
/// that have a shortcut key set.</returns>
|
||||||
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
||||||
{
|
{
|
||||||
if (CurrentContextMenu == null)
|
if (CurrentContextMenu is null)
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -140,7 +139,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||||
{
|
{
|
||||||
var keybindings = Keybindings();
|
var keybindings = Keybindings();
|
||||||
if (keybindings != null)
|
if (keybindings is not null)
|
||||||
{
|
{
|
||||||
// Does the pressed key match any of the keybindings?
|
// Does the pressed key match any of the keybindings?
|
||||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
||||||
@ -190,7 +189,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
OnPropertyChanging(nameof(CurrentContextMenu));
|
OnPropertyChanging(nameof(CurrentContextMenu));
|
||||||
OnPropertyChanged(nameof(CurrentContextMenu));
|
OnPropertyChanged(nameof(CurrentContextMenu));
|
||||||
|
|
||||||
if (CurrentContextMenu != null)
|
if (CurrentContextMenu is not null)
|
||||||
{
|
{
|
||||||
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
|
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
|
||||||
}
|
}
|
||||||
@ -198,7 +197,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
|
|
||||||
public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command)
|
public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command)
|
||||||
{
|
{
|
||||||
if (command == null)
|
if (command is null)
|
||||||
{
|
{
|
||||||
return ContextKeybindingResult.Unhandled;
|
return ContextKeybindingResult.Unhandled;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ public partial class DetailsCommandsViewModel(
|
|||||||
{
|
{
|
||||||
base.InitializeProperties();
|
base.InitializeProperties();
|
||||||
var model = _dataModel.Unsafe;
|
var model = _dataModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ public abstract partial class DetailsElementViewModel(IDetailsElement _detailsEl
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ public partial class DetailsLinkViewModel(
|
|||||||
|
|
||||||
public Uri? Link { get; private set; }
|
public Uri? Link { get; private set; }
|
||||||
|
|
||||||
public bool IsLink => Link != null;
|
public bool IsLink => Link is not null;
|
||||||
|
|
||||||
public bool IsText => !IsLink;
|
public bool IsText => !IsLink;
|
||||||
|
|
||||||
@ -26,14 +26,14 @@ public partial class DetailsLinkViewModel(
|
|||||||
{
|
{
|
||||||
base.InitializeProperties();
|
base.InitializeProperties();
|
||||||
var model = _dataModel.Unsafe;
|
var model = _dataModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Text = model.Text ?? string.Empty;
|
Text = model.Text ?? string.Empty;
|
||||||
Link = model.Link;
|
Link = model.Link;
|
||||||
if (string.IsNullOrEmpty(Text) && Link != null)
|
if (string.IsNullOrEmpty(Text) && Link is not null)
|
||||||
{
|
{
|
||||||
Text = Link.ToString();
|
Text = Link.ToString();
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ public partial class DetailsTagsViewModel(
|
|||||||
{
|
{
|
||||||
base.InitializeProperties();
|
base.InitializeProperties();
|
||||||
var model = _dataModel.Unsafe;
|
var model = _dataModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = _detailsModel.Unsafe;
|
var model = _detailsModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
|||||||
UpdateProperty(nameof(HeroImage));
|
UpdateProperty(nameof(HeroImage));
|
||||||
|
|
||||||
var meta = model.Metadata;
|
var meta = model.Metadata;
|
||||||
if (meta != null)
|
if (meta is not null)
|
||||||
{
|
{
|
||||||
foreach (var element in meta)
|
foreach (var element in meta)
|
||||||
{
|
{
|
||||||
@ -53,7 +53,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
|||||||
IDetailsTags => new DetailsTagsViewModel(element, this.PageContext),
|
IDetailsTags => new DetailsTagsViewModel(element, this.PageContext),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
if (vm != null)
|
if (vm is not null)
|
||||||
{
|
{
|
||||||
vm.InitializeProperties();
|
vm.InitializeProperties();
|
||||||
Metadata.Add(vm);
|
Metadata.Add(vm);
|
||||||
|
@ -4,12 +4,14 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||||
|
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||||
public interface IContextItemViewModel
|
public interface IContextItemViewModel
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
|||||||
// If the extension previously gave us a Data, then died, the data will
|
// If the extension previously gave us a Data, then died, the data will
|
||||||
// throw if we actually try to read it, but the pointer itself won't be
|
// throw if we actually try to read it, but the pointer itself won't be
|
||||||
// null, so this is relatively safe.
|
// null, so this is relatively safe.
|
||||||
public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe != null;
|
public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe is not null;
|
||||||
|
|
||||||
// Locally cached properties from IIconData.
|
// Locally cached properties from IIconData.
|
||||||
public string Icon { get; private set; } = string.Empty;
|
public string Icon { get; private set; } = string.Empty;
|
||||||
@ -36,7 +36,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
|||||||
public void InitializeProperties()
|
public void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo
|
|||||||
|
|
||||||
public bool HasIcon(bool light) => IconForTheme(light).HasIcon;
|
public bool HasIcon(bool light) => IconForTheme(light).HasIcon;
|
||||||
|
|
||||||
public bool IsSet => _model.Unsafe != null;
|
public bool IsSet => _model.Unsafe is not null;
|
||||||
|
|
||||||
IIconData? IIconInfo.Dark => Dark;
|
IIconData? IIconInfo.Dark => Dark;
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo
|
|||||||
public void InitializeProperties()
|
public void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
|||||||
public DetailsViewModel? Details { get; private set; }
|
public DetailsViewModel? Details { get; private set; }
|
||||||
|
|
||||||
[MemberNotNullWhen(true, nameof(Details))]
|
[MemberNotNullWhen(true, nameof(Details))]
|
||||||
public bool HasDetails => Details != null;
|
public bool HasDetails => Details is not null;
|
||||||
|
|
||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
@ -40,7 +40,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
|||||||
base.InitializeProperties();
|
base.InitializeProperties();
|
||||||
|
|
||||||
var li = Model.Unsafe;
|
var li = Model.Unsafe;
|
||||||
if (li == null)
|
if (li is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
|||||||
TextToSuggest = li.TextToSuggest;
|
TextToSuggest = li.TextToSuggest;
|
||||||
Section = li.Section ?? string.Empty;
|
Section = li.Section ?? string.Empty;
|
||||||
var extensionDetails = li.Details;
|
var extensionDetails = li.Details;
|
||||||
if (extensionDetails != null)
|
if (extensionDetails is not null)
|
||||||
{
|
{
|
||||||
Details = new(extensionDetails, PageContext);
|
Details = new(extensionDetails, PageContext);
|
||||||
Details.InitializeProperties();
|
Details.InitializeProperties();
|
||||||
@ -67,7 +67,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
|||||||
base.FetchProperty(propertyName);
|
base.FetchProperty(propertyName);
|
||||||
|
|
||||||
var model = this.Model.Unsafe;
|
var model = this.Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
|||||||
break;
|
break;
|
||||||
case nameof(Details):
|
case nameof(Details):
|
||||||
var extensionDetails = model.Details;
|
var extensionDetails = model.Details;
|
||||||
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
|
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||||
Details?.InitializeProperties();
|
Details?.InitializeProperties();
|
||||||
UpdateProperty(nameof(Details));
|
UpdateProperty(nameof(Details));
|
||||||
UpdateProperty(nameof(HasDetails));
|
UpdateProperty(nameof(HasDetails));
|
||||||
@ -136,7 +136,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
|||||||
Details?.SafeCleanup();
|
Details?.SafeCleanup();
|
||||||
|
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
// We don't need to revoke the PropChanged event handler here,
|
// We don't need to revoke the PropChanged event handler here,
|
||||||
// because we are just overriding CommandItem's FetchProperty and
|
// because we are just overriding CommandItem's FetchProperty and
|
||||||
|
@ -63,6 +63,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
|
|
||||||
private Task? _initializeItemsTask;
|
private Task? _initializeItemsTask;
|
||||||
private CancellationTokenSource? _cancellationTokenSource;
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
|
private CancellationTokenSource? _fetchItemsCancellationTokenSource;
|
||||||
|
|
||||||
private ListItemViewModel? _lastSelectedItem;
|
private ListItemViewModel? _lastSelectedItem;
|
||||||
|
|
||||||
@ -129,22 +130,38 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
||||||
private void FetchItems()
|
private void FetchItems()
|
||||||
{
|
{
|
||||||
|
// Cancel any previous FetchItems operation
|
||||||
|
_fetchItemsCancellationTokenSource?.Cancel();
|
||||||
|
_fetchItemsCancellationTokenSource?.Dispose();
|
||||||
|
_fetchItemsCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var cancellationToken = _fetchItemsCancellationTokenSource.Token;
|
||||||
|
|
||||||
// TEMPORARY: just plop all the items into a single group
|
// TEMPORARY: just plop all the items into a single group
|
||||||
// see 9806fe5d8 for the last commit that had this with sections
|
// see 9806fe5d8 for the last commit that had this with sections
|
||||||
_isFetching = true;
|
_isFetching = true;
|
||||||
|
|
||||||
|
// Collect all the items into new viewmodels
|
||||||
|
Collection<ListItemViewModel> newViewModels = [];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Check for cancellation before starting expensive operations
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var newItems = _model.Unsafe!.GetItems();
|
var newItems = _model.Unsafe!.GetItems();
|
||||||
|
|
||||||
// Collect all the items into new viewmodels
|
// Check for cancellation after getting items from extension
|
||||||
Collection<ListItemViewModel> newViewModels = [];
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// TODO we can probably further optimize this by also keeping a
|
// TODO we can probably further optimize this by also keeping a
|
||||||
// HashSet of every ExtensionObject we currently have, and only
|
// HashSet of every ExtensionObject we currently have, and only
|
||||||
// building new viewmodels for the ones we haven't already built.
|
// building new viewmodels for the ones we haven't already built.
|
||||||
foreach (var item in newItems)
|
foreach (var item in newItems)
|
||||||
{
|
{
|
||||||
|
// Check for cancellation during item processing
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
ListItemViewModel viewModel = new(item, new(this));
|
ListItemViewModel viewModel = new(item, new(this));
|
||||||
|
|
||||||
// If an item fails to load, silently ignore it.
|
// If an item fails to load, silently ignore it.
|
||||||
@ -154,25 +171,57 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for cancellation before initializing first twenty items
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var firstTwenty = newViewModels.Take(20);
|
var firstTwenty = newViewModels.Take(20);
|
||||||
foreach (var item in firstTwenty)
|
foreach (var item in firstTwenty)
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
item?.SafeInitializeProperties();
|
item?.SafeInitializeProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel any ongoing search
|
// Cancel any ongoing search
|
||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
|
// Check for cancellation before updating the list
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
List<ListItemViewModel> removedItems = [];
|
||||||
lock (_listLock)
|
lock (_listLock)
|
||||||
{
|
{
|
||||||
// Now that we have new ViewModels for everything from the
|
// Now that we have new ViewModels for everything from the
|
||||||
// extension, smartly update our list of VMs
|
// extension, smartly update our list of VMs
|
||||||
ListHelpers.InPlaceUpdateList(Items, newViewModels);
|
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems);
|
||||||
|
|
||||||
|
// DO NOT ThrowIfCancellationRequested AFTER THIS! If you do,
|
||||||
|
// you'll clean up list items that we've now transferred into
|
||||||
|
// .Items
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we removed items, we need to clean them up, to remove our event handlers
|
||||||
|
foreach (var removedItem in removedItems)
|
||||||
|
{
|
||||||
|
removedItem.SafeCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Iterate over everything in Items, and prune items from the
|
// TODO: Iterate over everything in Items, and prune items from the
|
||||||
// cache if we don't need them anymore
|
// cache if we don't need them anymore
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Cancellation is expected, don't treat as error
|
||||||
|
|
||||||
|
// However, if we were cancelled, we didn't actually add these items to
|
||||||
|
// our Items list. Before we release them to the GC, make sure we clean
|
||||||
|
// them up
|
||||||
|
foreach (var vm in newViewModels)
|
||||||
|
{
|
||||||
|
vm.SafeCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// TODO: Move this within the for loop, so we can catch issues with individual items
|
// TODO: Move this within the for loop, so we can catch issues with individual items
|
||||||
@ -298,11 +347,11 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void InvokeItem(ListItemViewModel? item)
|
private void InvokeItem(ListItemViewModel? item)
|
||||||
{
|
{
|
||||||
if (item != null)
|
if (item is not null)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
|
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
|
||||||
}
|
}
|
||||||
else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe != null)
|
else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe is not null)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
|
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
|
||||||
EmptyContent.PrimaryCommand.Command.Model,
|
EmptyContent.PrimaryCommand.Command.Model,
|
||||||
@ -314,14 +363,14 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void InvokeSecondaryCommand(ListItemViewModel? item)
|
private void InvokeSecondaryCommand(ListItemViewModel? item)
|
||||||
{
|
{
|
||||||
if (item != null)
|
if (item is not null)
|
||||||
{
|
{
|
||||||
if (item.SecondaryCommand != null)
|
if (item.SecondaryCommand is not null)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.SecondaryCommand.Command.Model, item.Model));
|
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.SecondaryCommand.Command.Model, item.Model));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null)
|
else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe is not null)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
|
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
|
||||||
EmptyContent.SecondaryCommand.Command.Model,
|
EmptyContent.SecondaryCommand.Command.Model,
|
||||||
@ -332,12 +381,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void UpdateSelectedItem(ListItemViewModel? item)
|
private void UpdateSelectedItem(ListItemViewModel? item)
|
||||||
{
|
{
|
||||||
if (_lastSelectedItem != null)
|
if (_lastSelectedItem is not null)
|
||||||
{
|
{
|
||||||
_lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
|
_lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item != null)
|
if (item is not null)
|
||||||
{
|
{
|
||||||
SetSelectedItem(item);
|
SetSelectedItem(item);
|
||||||
}
|
}
|
||||||
@ -383,7 +432,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
var item = _lastSelectedItem;
|
var item = _lastSelectedItem;
|
||||||
if (item == null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -438,7 +487,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
base.InitializeProperties();
|
base.InitializeProperties();
|
||||||
|
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -465,7 +514,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
public void LoadMoreIfNeeded()
|
public void LoadMoreIfNeeded()
|
||||||
{
|
{
|
||||||
var model = this._model.Unsafe;
|
var model = this._model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -509,7 +558,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
base.FetchProperty(propertyName);
|
base.FetchProperty(propertyName);
|
||||||
|
|
||||||
var model = this._model.Unsafe;
|
var model = this._model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -540,7 +589,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
private void UpdateEmptyContent()
|
private void UpdateEmptyContent()
|
||||||
{
|
{
|
||||||
UpdateProperty(nameof(ShowEmptyContent));
|
UpdateProperty(nameof(ShowEmptyContent));
|
||||||
if (!ShowEmptyContent || EmptyContent.Model.Unsafe == null)
|
if (!ShowEmptyContent || EmptyContent.Model.Unsafe is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -560,6 +609,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
_cancellationTokenSource?.Dispose();
|
_cancellationTokenSource?.Dispose();
|
||||||
_cancellationTokenSource = null;
|
_cancellationTokenSource = null;
|
||||||
|
|
||||||
|
_fetchItemsCancellationTokenSource?.Cancel();
|
||||||
|
_fetchItemsCancellationTokenSource?.Dispose();
|
||||||
|
_fetchItemsCancellationTokenSource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UnsafeCleanup()
|
protected override void UnsafeCleanup()
|
||||||
@ -570,6 +623,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
EmptyContent = new(new(null), PageContext); // necessary?
|
EmptyContent = new(new(null), PageContext); // necessary?
|
||||||
|
|
||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
|
_fetchItemsCancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
lock (_listLock)
|
lock (_listLock)
|
||||||
{
|
{
|
||||||
@ -588,7 +642,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
model.ItemsChanged -= Model_ItemsChanged;
|
model.ItemsChanged -= Model_ItemsChanged;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ public partial class LogMessageViewModel : ExtensionObjectViewModel
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Microsoft.CmdPal.Common.Helpers;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial AppExtensionHost ExtensionHost { get; private set; }
|
public partial AppExtensionHost ExtensionHost { get; private set; }
|
||||||
|
|
||||||
public bool HasStatusMessage => MostRecentStatusMessage != null;
|
public bool HasStatusMessage => MostRecentStatusMessage is not null;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(HasStatusMessage))]
|
[NotifyPropertyChangedFor(nameof(HasStatusMessage))]
|
||||||
@ -132,7 +133,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var page = _pageModel.Unsafe;
|
var page = _pageModel.Unsafe;
|
||||||
if (page == null)
|
if (page is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -177,7 +178,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
|||||||
protected virtual void FetchProperty(string propertyName)
|
protected virtual void FetchProperty(string propertyName)
|
||||||
{
|
{
|
||||||
var model = this._pageModel.Unsafe;
|
var model = this._pageModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -223,9 +224,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
|||||||
extensionHint ??= ExtensionHost.GetExtensionDisplayName() ?? Title;
|
extensionHint ??= ExtensionHost.GetExtensionDisplayName() ?? Title;
|
||||||
Task.Factory.StartNew(
|
Task.Factory.StartNew(
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
ErrorMessage += $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n";
|
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint);
|
||||||
},
|
ErrorMessage += message;
|
||||||
|
},
|
||||||
CancellationToken.None,
|
CancellationToken.None,
|
||||||
TaskCreationOptions.None,
|
TaskCreationOptions.None,
|
||||||
Scheduler);
|
Scheduler);
|
||||||
@ -240,7 +242,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
|||||||
ExtensionHost.StatusMessages.CollectionChanged -= StatusMessages_CollectionChanged;
|
ExtensionHost.StatusMessages.CollectionChanged -= StatusMessages_CollectionChanged;
|
||||||
|
|
||||||
var model = _pageModel.Unsafe;
|
var model = _pageModel.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
model.PropChanged -= Model_PropChanged;
|
model.PropChanged -= Model_PropChanged;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel
|
|||||||
protected virtual void FetchProperty(string propertyName)
|
protected virtual void FetchProperty(string propertyName)
|
||||||
{
|
{
|
||||||
var model = this.Model.Unsafe;
|
var model = this.Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||||
|
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||||
public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem
|
public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ public partial class ShellViewModel : ObservableObject,
|
|||||||
|
|
||||||
////LoadedState = ViewModelLoadedState.Loading;
|
////LoadedState = ViewModelLoadedState.Loading;
|
||||||
if (!viewModel.IsInitialized
|
if (!viewModel.IsInitialized
|
||||||
&& viewModel.InitializeCommand != null)
|
&& viewModel.InitializeCommand is not null)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@ -185,7 +185,7 @@ public partial class ShellViewModel : ObservableObject,
|
|||||||
private void PerformCommand(PerformCommandMessage message)
|
private void PerformCommand(PerformCommandMessage message)
|
||||||
{
|
{
|
||||||
var command = message.Command.Unsafe;
|
var command = message.Command.Unsafe;
|
||||||
if (command == null)
|
if (command is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -205,7 +205,7 @@ public partial class ShellViewModel : ObservableObject,
|
|||||||
|
|
||||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||||
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host);
|
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host);
|
||||||
if (pageViewModel == null)
|
if (pageViewModel is null)
|
||||||
{
|
{
|
||||||
Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
|
Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
@ -240,7 +240,7 @@ public partial class ShellViewModel : ObservableObject,
|
|||||||
// TODO GH #525 This needs more better locking.
|
// TODO GH #525 This needs more better locking.
|
||||||
lock (_invokeLock)
|
lock (_invokeLock)
|
||||||
{
|
{
|
||||||
if (_handleInvokeTask != null)
|
if (_handleInvokeTask is not null)
|
||||||
{
|
{
|
||||||
// do nothing - a command is already doing a thing
|
// do nothing - a command is already doing a thing
|
||||||
}
|
}
|
||||||
@ -280,7 +280,7 @@ public partial class ShellViewModel : ObservableObject,
|
|||||||
|
|
||||||
private void UnsafeHandleCommandResult(ICommandResult? result)
|
private void UnsafeHandleCommandResult(ICommandResult? result)
|
||||||
{
|
{
|
||||||
if (result == null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
// No result, nothing to do.
|
// No result, nothing to do.
|
||||||
return;
|
return;
|
||||||
|
@ -17,7 +17,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
|||||||
|
|
||||||
public ProgressViewModel? Progress { get; private set; }
|
public ProgressViewModel? Progress { get; private set; }
|
||||||
|
|
||||||
public bool HasProgress => Progress != null;
|
public bool HasProgress => Progress is not null;
|
||||||
|
|
||||||
public StatusMessageViewModel(IStatusMessage message, WeakReference<IPageContext> context)
|
public StatusMessageViewModel(IStatusMessage message, WeakReference<IPageContext> context)
|
||||||
: base(context)
|
: base(context)
|
||||||
@ -28,7 +28,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
|||||||
Message = model.Message;
|
Message = model.Message;
|
||||||
State = model.State;
|
State = model.State;
|
||||||
var modelProgress = model.Progress;
|
var modelProgress = model.Progress;
|
||||||
if (modelProgress != null)
|
if (modelProgress is not null)
|
||||||
{
|
{
|
||||||
Progress = new(modelProgress, this.PageContext);
|
Progress = new(modelProgress, this.PageContext);
|
||||||
Progress.InitializeProperties();
|
Progress.InitializeProperties();
|
||||||
@ -61,7 +61,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
|||||||
protected virtual void FetchProperty(string propertyName)
|
protected virtual void FetchProperty(string propertyName)
|
||||||
{
|
{
|
||||||
var model = this.Model.Unsafe;
|
var model = this.Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
|||||||
break;
|
break;
|
||||||
case nameof(Progress):
|
case nameof(Progress):
|
||||||
var modelProgress = model.Progress;
|
var modelProgress = model.Progress;
|
||||||
if (modelProgress != null)
|
if (modelProgress is not null)
|
||||||
{
|
{
|
||||||
Progress = new(modelProgress, this.PageContext);
|
Progress = new(modelProgress, this.PageContext);
|
||||||
Progress.InitializeProperties();
|
Progress.InitializeProperties();
|
||||||
|
@ -28,7 +28,7 @@ public partial class TagViewModel(ITag _tag, WeakReference<IPageContext> context
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = _tagModel.Unsafe;
|
var model = _tagModel.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ public partial class AliasManager : ObservableObject
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
|
var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
|
||||||
if (topLevelCommand != null)
|
if (topLevelCommand is not null)
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
|
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ public partial class AliasManager : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we already have _this exact alias_, do nothing
|
// If we already have _this exact alias_, do nothing
|
||||||
if (newAlias != null &&
|
if (newAlias is not null &&
|
||||||
_aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
_aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
||||||
{
|
{
|
||||||
if (existingAlias.CommandId == commandId)
|
if (existingAlias.CommandId == commandId)
|
||||||
@ -113,7 +113,7 @@ public partial class AliasManager : ObservableObject
|
|||||||
_aliases.Remove(alias.SearchPrefix);
|
_aliases.Remove(alias.SearchPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newAlias != null)
|
if (newAlias is not null)
|
||||||
{
|
{
|
||||||
AddAlias(newAlias);
|
AddAlias(newAlias);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ public partial class AppStateModel : ObservableObject
|
|||||||
|
|
||||||
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
|
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
|
||||||
|
|
||||||
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
|
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
|
||||||
|
|
||||||
return loaded ?? new();
|
return loaded ?? new();
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
|||||||
|
|
||||||
public sealed class CommandProviderWrapper
|
public sealed class CommandProviderWrapper
|
||||||
{
|
{
|
||||||
public bool IsExtension => Extension != null;
|
public bool IsExtension => Extension is not null;
|
||||||
|
|
||||||
private readonly bool isValid;
|
private readonly bool isValid;
|
||||||
|
|
||||||
@ -188,14 +188,14 @@ public sealed class CommandProviderWrapper
|
|||||||
|
|
||||||
return topLevelViewModel;
|
return topLevelViewModel;
|
||||||
};
|
};
|
||||||
if (commands != null)
|
if (commands is not null)
|
||||||
{
|
{
|
||||||
TopLevelItems = commands
|
TopLevelItems = commands
|
||||||
.Select(c => makeAndAdd(c, false))
|
.Select(c => makeAndAdd(c, false))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fallbacks != null)
|
if (fallbacks is not null)
|
||||||
{
|
{
|
||||||
FallbackItems = fallbacks
|
FallbackItems = fallbacks
|
||||||
.Select(c => makeAndAdd(c, true))
|
.Select(c => makeAndAdd(c, true))
|
||||||
|
@ -18,18 +18,18 @@ public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings,
|
|||||||
public bool Initialized { get; private set; }
|
public bool Initialized { get; private set; }
|
||||||
|
|
||||||
public bool HasSettings =>
|
public bool HasSettings =>
|
||||||
_model.Unsafe != null && // We have a settings model AND
|
_model.Unsafe is not null && // We have a settings model AND
|
||||||
(!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page
|
(!Initialized || SettingsPage is not null); // we weren't initialized, OR we were, and we do have a settings page
|
||||||
|
|
||||||
private void UnsafeInitializeProperties()
|
private void UnsafeInitializeProperties()
|
||||||
{
|
{
|
||||||
var model = _model.Unsafe;
|
var model = _model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.SettingsPage != null)
|
if (model.SettingsPage is not null)
|
||||||
{
|
{
|
||||||
SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost);
|
SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost);
|
||||||
SettingsPage.InitializeProperties();
|
SettingsPage.InitializeProperties();
|
||||||
|
@ -30,7 +30,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
|
|||||||
public override ICommandResult SubmitForm(string inputs, string data)
|
public override ICommandResult SubmitForm(string inputs, string data)
|
||||||
{
|
{
|
||||||
var dataInput = JsonNode.Parse(data)?.AsObject();
|
var dataInput = JsonNode.Parse(data)?.AsObject();
|
||||||
if (dataInput == null)
|
if (dataInput is null)
|
||||||
{
|
{
|
||||||
return CommandResult.KeepOpen();
|
return CommandResult.KeepOpen();
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ public partial class LogMessagesPage : ListPage
|
|||||||
|
|
||||||
private void LogMessages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
private void LogMessages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
|
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems is not null)
|
||||||
{
|
{
|
||||||
foreach (var item in e.NewItems)
|
foreach (var item in e.NewItems)
|
||||||
{
|
{
|
||||||
|
@ -9,6 +9,7 @@ using ManagedCommon;
|
|||||||
using Microsoft.CmdPal.Common.Helpers;
|
using Microsoft.CmdPal.Common.Helpers;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.Ext.Apps;
|
using Microsoft.CmdPal.Ext.Apps;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -60,6 +61,7 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
var settings = _serviceProvider.GetService<SettingsModel>()!;
|
var settings = _serviceProvider.GetService<SettingsModel>()!;
|
||||||
settings.SettingsChanged += SettingsChangedHandler;
|
settings.SettingsChanged += SettingsChangedHandler;
|
||||||
HotReloadSettings(settings);
|
HotReloadSettings(settings);
|
||||||
|
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
|
||||||
|
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
}
|
}
|
||||||
@ -201,7 +203,7 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
|
|
||||||
// If we don't have any previous filter results to work with, start
|
// If we don't have any previous filter results to work with, start
|
||||||
// with a list of all our commands & apps.
|
// with a list of all our commands & apps.
|
||||||
if (_filteredItems == null)
|
if (_filteredItems is null)
|
||||||
{
|
{
|
||||||
_filteredItems = commands;
|
_filteredItems = commands;
|
||||||
_filteredItemsIncludesApps = _includeApps;
|
_filteredItemsIncludesApps = _includeApps;
|
||||||
|
@ -98,7 +98,7 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
|||||||
public override CommandResult SubmitForm(string payload)
|
public override CommandResult SubmitForm(string payload)
|
||||||
{
|
{
|
||||||
var formInput = JsonNode.Parse(payload)?.AsObject();
|
var formInput = JsonNode.Parse(payload)?.AsObject();
|
||||||
if (formInput == null)
|
if (formInput is null)
|
||||||
{
|
{
|
||||||
return CommandResult.KeepOpen();
|
return CommandResult.KeepOpen();
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ public partial class NewExtensionPage : ContentPage
|
|||||||
|
|
||||||
public override IContent[] GetContent()
|
public override IContent[] GetContent()
|
||||||
{
|
{
|
||||||
return _resultForm != null ? [_resultForm] : [_inputForm];
|
return _resultForm is not null ? [_resultForm] : [_inputForm];
|
||||||
}
|
}
|
||||||
|
|
||||||
public NewExtensionPage()
|
public NewExtensionPage()
|
||||||
@ -28,13 +28,13 @@ public partial class NewExtensionPage : ContentPage
|
|||||||
|
|
||||||
private void FormSubmitted(NewExtensionFormBase sender, NewExtensionFormBase? args)
|
private void FormSubmitted(NewExtensionFormBase sender, NewExtensionFormBase? args)
|
||||||
{
|
{
|
||||||
if (_resultForm != null)
|
if (_resultForm is not null)
|
||||||
{
|
{
|
||||||
_resultForm.FormSubmitted -= FormSubmitted;
|
_resultForm.FormSubmitted -= FormSubmitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
_resultForm = args;
|
_resultForm = args;
|
||||||
if (_resultForm != null)
|
if (_resultForm is not null)
|
||||||
{
|
{
|
||||||
_resultForm.FormSubmitted += FormSubmitted;
|
_resultForm.FormSubmitted += FormSubmitted;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AdaptiveCards.ObjectModel.WinUI3;
|
using AdaptiveCards.ObjectModel.WinUI3;
|
||||||
using AdaptiveCards.Templating;
|
using AdaptiveCards.Templating;
|
||||||
@ -96,109 +97,40 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
|||||||
UpdateProperty(nameof(Card));
|
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)
|
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
|
||||||
{
|
{
|
||||||
// BODGY circa GH #40979
|
if (action is AdaptiveOpenUrlAction openUrlAction)
|
||||||
// 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))
|
|
||||||
{
|
{
|
||||||
var actionTypeString = actionTypeValue.GetString();
|
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
|
||||||
Logger.LogTrace($"atString={actionTypeString}");
|
return;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var inputString = inputs.Stringify();
|
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
|
||||||
_ = Task.Run(() =>
|
|
||||||
{
|
{
|
||||||
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!;
|
try
|
||||||
if (model != null)
|
|
||||||
{
|
{
|
||||||
var result = model.SubmitForm(inputString, dataString);
|
var model = _formModel.Unsafe!;
|
||||||
Logger.LogDebug($"SubmitForm() returned {result}");
|
if (model != null)
|
||||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
{
|
||||||
|
var result = model.SubmitForm(inputString, dataString);
|
||||||
|
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
ShowException(ex);
|
||||||
ShowException(ex);
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly string ErrorCardJson = """
|
private static readonly string ErrorCardJson = """
|
||||||
|
@ -20,7 +20,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -47,7 +47,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
|
|||||||
protected void FetchProperty(string propertyName)
|
protected void FetchProperty(string propertyName)
|
||||||
{
|
{
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -66,7 +66,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
|
|||||||
{
|
{
|
||||||
base.UnsafeCleanup();
|
base.UnsafeCleanup();
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
model.PropChanged -= Model_PropChanged;
|
model.PropChanged -= Model_PropChanged;
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,13 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
|||||||
public override void InitializeProperties()
|
public override void InitializeProperties()
|
||||||
{
|
{
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var root = model.RootContent;
|
var root = model.RootContent;
|
||||||
if (root != null)
|
if (root is not null)
|
||||||
{
|
{
|
||||||
RootContent = ViewModelFromContent(root, PageContext);
|
RootContent = ViewModelFromContent(root, PageContext);
|
||||||
RootContent?.InitializeProperties();
|
RootContent?.InitializeProperties();
|
||||||
@ -82,7 +82,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
|||||||
protected void FetchProperty(string propertyName)
|
protected void FetchProperty(string propertyName)
|
||||||
{
|
{
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model == null)
|
if (model is null)
|
||||||
{
|
{
|
||||||
return; // throw?
|
return; // throw?
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
|||||||
{
|
{
|
||||||
case nameof(RootContent):
|
case nameof(RootContent):
|
||||||
var root = model.RootContent;
|
var root = model.RootContent;
|
||||||
if (root != null)
|
if (root is not null)
|
||||||
{
|
{
|
||||||
RootContent = ViewModelFromContent(root, PageContext);
|
RootContent = ViewModelFromContent(root, PageContext);
|
||||||
}
|
}
|
||||||
@ -119,7 +119,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
|||||||
foreach (var item in newItems)
|
foreach (var item in newItems)
|
||||||
{
|
{
|
||||||
var viewModel = ViewModelFromContent(item, PageContext);
|
var viewModel = ViewModelFromContent(item, PageContext);
|
||||||
if (viewModel != null)
|
if (viewModel is not null)
|
||||||
{
|
{
|
||||||
viewModel.InitializeProperties();
|
viewModel.InitializeProperties();
|
||||||
newContent.Add(viewModel);
|
newContent.Add(viewModel);
|
||||||
@ -153,7 +153,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
|||||||
|
|
||||||
Children.Clear();
|
Children.Clear();
|
||||||
var model = Model.Unsafe;
|
var model = Model.Unsafe;
|
||||||
if (model != null)
|
if (model is not null)
|
||||||
{
|
{
|
||||||
model.PropChanged -= Model_PropChanged;
|
model.PropChanged -= Model_PropChanged;
|
||||||
model.ItemsChanged -= Model_ItemsChanged;
|
model.ItemsChanged -= Model_ItemsChanged;
|
||||||
|
@ -29,7 +29,7 @@ public partial class HotkeyManager : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_commandHotkeys.RemoveAll(item => item.Hotkey == null);
|
_commandHotkeys.RemoveAll(item => item.Hotkey is null);
|
||||||
|
|
||||||
foreach (var item in _commandHotkeys)
|
foreach (var item in _commandHotkeys)
|
||||||
{
|
{
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
namespace Microsoft.CmdPal.UI.Messages;
|
||||||
|
|
||||||
public record OpenSettingsMessage()
|
public record OpenSettingsMessage()
|
||||||
{
|
{
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Message which closes the application. Used by <see cref="QuitCommand"/> via <see cref="BuiltInsCommandProvider"/>.
|
/// Message which closes the application. Used by <see cref="QuitCommand"/> via <see cref="BuiltInsCommandProvider"/>.
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
|
|
||||||
public record ReloadCommandsMessage()
|
public record ReloadCommandsMessage()
|
||||||
{
|
{
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
|
|
||||||
public record UpdateFallbackItemsMessage()
|
public record UpdateFallbackItemsMessage()
|
||||||
{
|
{
|
@ -90,7 +90,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
|||||||
}).Result;
|
}).Result;
|
||||||
var isExtension = isCmdPalExtensionResult.IsExtension;
|
var isExtension = isCmdPalExtensionResult.IsExtension;
|
||||||
var extension = isCmdPalExtensionResult.Extension;
|
var extension = isCmdPalExtensionResult.Extension;
|
||||||
if (isExtension && extension != null)
|
if (isExtension && extension is not null)
|
||||||
{
|
{
|
||||||
CommandPaletteHost.Instance.DebugLog($"Installed new extension app {extension.DisplayName}");
|
CommandPaletteHost.Instance.DebugLog($"Installed new extension app {extension.DisplayName}");
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
|||||||
{
|
{
|
||||||
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
|
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
|
||||||
|
|
||||||
return new(cmdPalProvider != null && classId.Count != 0, extension);
|
return new(cmdPalProvider is not null && classId.Count != 0, extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
|||||||
{
|
{
|
||||||
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
|
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
|
||||||
|
|
||||||
if (cmdPalProvider == null || classIds.Count == 0)
|
if (cmdPalProvider is null || classIds.Count == 0)
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -352,12 +352,12 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
|||||||
{
|
{
|
||||||
var propSetList = new List<string>();
|
var propSetList = new List<string>();
|
||||||
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
|
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
|
||||||
if (singlePropertySet != null)
|
if (singlePropertySet is not null)
|
||||||
{
|
{
|
||||||
var classId = GetProperty(singlePropertySet, ClassIdProperty);
|
var classId = GetProperty(singlePropertySet, ClassIdProperty);
|
||||||
|
|
||||||
// If the instance has a classId as a single string, then it's only supporting a single instance.
|
// If the instance has a classId as a single string, then it's only supporting a single instance.
|
||||||
if (classId != null)
|
if (classId is not null)
|
||||||
{
|
{
|
||||||
propSetList.Add(classId);
|
propSetList.Add(classId);
|
||||||
}
|
}
|
||||||
@ -365,7 +365,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
|
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
|
||||||
if (propertySetArray != null)
|
if (propertySetArray is not null)
|
||||||
{
|
{
|
||||||
foreach (var prop in propertySetArray)
|
foreach (var prop in propertySetArray)
|
||||||
{
|
{
|
||||||
@ -375,7 +375,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var classId = GetProperty(propertySet, ClassIdProperty);
|
var classId = GetProperty(propertySet, ClassIdProperty);
|
||||||
if (classId != null)
|
if (classId is not null)
|
||||||
{
|
{
|
||||||
propSetList.Add(classId);
|
propSetList.Add(classId);
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ public class ProviderSettings
|
|||||||
public void Connect(CommandProviderWrapper wrapper)
|
public void Connect(CommandProviderWrapper wrapper)
|
||||||
{
|
{
|
||||||
ProviderId = wrapper.ProviderId;
|
ProviderId = wrapper.ProviderId;
|
||||||
IsBuiltin = wrapper.Extension == null;
|
IsBuiltin = wrapper.Extension is null;
|
||||||
|
|
||||||
ProviderDisplayName = wrapper.DisplayName;
|
ProviderDisplayName = wrapper.DisplayName;
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.Common.Services;
|
using Microsoft.CmdPal.Common.Services;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ public partial class ProviderSettingsViewModel(
|
|||||||
Resources.builtin_disabled_extension;
|
Resources.builtin_disabled_extension;
|
||||||
|
|
||||||
[MemberNotNullWhen(true, nameof(Extension))]
|
[MemberNotNullWhen(true, nameof(Extension))]
|
||||||
public bool IsFromExtension => _provider.Extension != null;
|
public bool IsFromExtension => _provider.Extension is not null;
|
||||||
|
|
||||||
public IExtensionWrapper? Extension => _provider.Extension;
|
public IExtensionWrapper? Extension => _provider.Extension;
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ public partial class ProviderSettingsViewModel(
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_provider.Settings == null)
|
if (_provider.Settings is null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ public partial class ProviderSettingsViewModel(
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_provider.Settings == null)
|
if (_provider.Settings is null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ public partial class ProviderSettingsViewModel(
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (field == null)
|
if (field is null)
|
||||||
{
|
{
|
||||||
field = BuildTopLevelViewModels();
|
field = BuildTopLevelViewModels();
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ public partial class ProviderSettingsViewModel(
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (field == null)
|
if (field is null)
|
||||||
{
|
{
|
||||||
field = BuildFallbackViewModels();
|
field = BuildFallbackViewModels();
|
||||||
}
|
}
|
||||||
@ -173,7 +173,7 @@ public partial class ProviderSettingsViewModel(
|
|||||||
|
|
||||||
private void InitializeSettingsPage()
|
private void InitializeSettingsPage()
|
||||||
{
|
{
|
||||||
if (_provider.Settings == null)
|
if (_provider.Settings is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ public partial class RecentCommandsManager : ObservableObject
|
|||||||
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
|
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
|
||||||
// match after one use.
|
// match after one use.
|
||||||
// Usually it has a weight of 84, compared to 109 for the VS cmd prompt
|
// Usually it has a weight of 84, compared to 109 for the VS cmd prompt
|
||||||
if (entry.Item != null)
|
if (entry.Item is not null)
|
||||||
{
|
{
|
||||||
var index = entry.Index;
|
var index = entry.Index;
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ public partial class RecentCommandsManager : ObservableObject
|
|||||||
var entry = History
|
var entry = History
|
||||||
.Where(item => item.CommandId == commandId)
|
.Where(item => item.CommandId == commandId)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
if (entry == null)
|
if (entry is null)
|
||||||
{
|
{
|
||||||
var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 };
|
var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 };
|
||||||
History.Insert(0, newitem);
|
History.Insert(0, newitem);
|
||||||
|
@ -95,7 +95,7 @@ public partial class SettingsModel : ObservableObject
|
|||||||
|
|
||||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
|
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
|
||||||
|
|
||||||
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
|
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
|
||||||
|
|
||||||
return loaded ?? new();
|
return loaded ?? new();
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ using ManagedCommon;
|
|||||||
using Microsoft.CmdPal.Common.Helpers;
|
using Microsoft.CmdPal.Common.Helpers;
|
||||||
using Microsoft.CmdPal.Common.Services;
|
using Microsoft.CmdPal.Common.Services;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -151,7 +151,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
|||||||
WeakReference<IPageContext> weakSelf = new(this);
|
WeakReference<IPageContext> weakSelf = new(this);
|
||||||
await sender.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
await sender.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
||||||
|
|
||||||
List<TopLevelViewModel> newItems = [..sender.TopLevelItems];
|
List<TopLevelViewModel> newItems = [.. sender.TopLevelItems];
|
||||||
foreach (var i in sender.FallbackItems)
|
foreach (var i in sender.FallbackItems)
|
||||||
{
|
{
|
||||||
if (i.IsEnabled)
|
if (i.IsEnabled)
|
||||||
@ -170,20 +170,29 @@ public partial class TopLevelCommandManager : ObservableObject,
|
|||||||
// TODO: just added a lock around all of this anyway, but keeping the clone
|
// TODO: just added a lock around all of this anyway, but keeping the clone
|
||||||
// while looking on some other ways to improve this; can be removed later.
|
// while looking on some other ways to improve this; can be removed later.
|
||||||
List<TopLevelViewModel> clone = [.. TopLevelCommands];
|
List<TopLevelViewModel> clone = [.. TopLevelCommands];
|
||||||
var startIndex = -1;
|
|
||||||
|
|
||||||
|
var startIndex = FindIndexForFirstProviderItem(clone, sender.ProviderId);
|
||||||
|
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
||||||
|
clone.InsertRange(startIndex, newItems);
|
||||||
|
|
||||||
|
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
static int FindIndexForFirstProviderItem(List<TopLevelViewModel> topLevelItems, string providerId)
|
||||||
|
{
|
||||||
// Tricky: all Commands from a single provider get added to the
|
// Tricky: all Commands from a single provider get added to the
|
||||||
// top-level list all together, in a row. So if we find just the first
|
// top-level list all together, in a row. So if we find just the first
|
||||||
// one, we can slice it out and insert the new ones there.
|
// one, we can slice it out and insert the new ones there.
|
||||||
for (var i = 0; i < clone.Count; i++)
|
for (var i = 0; i < topLevelItems.Count; i++)
|
||||||
{
|
{
|
||||||
var wrapper = clone[i];
|
var wrapper = topLevelItems[i];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (sender.ProviderId == wrapper.CommandProviderId)
|
if (providerId == wrapper.CommandProviderId)
|
||||||
{
|
{
|
||||||
startIndex = i;
|
return i;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@ -191,9 +200,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
// If we didn't find any, then we just append the new commands to the end of the list.
|
||||||
clone.InsertRange(startIndex, newItems);
|
return topLevelItems.Count;
|
||||||
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +249,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
|||||||
_extensionCommandProviders.Clear();
|
_extensionCommandProviders.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensions != null)
|
if (extensions is not null)
|
||||||
{
|
{
|
||||||
await StartExtensionsAndGetCommands(extensions);
|
await StartExtensionsAndGetCommands(extensions);
|
||||||
}
|
}
|
||||||
@ -275,7 +283,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
|||||||
var startTasks = extensions.Select(StartExtensionWithTimeoutAsync);
|
var startTasks = extensions.Select(StartExtensionWithTimeoutAsync);
|
||||||
|
|
||||||
// Wait for all extensions to start
|
// Wait for all extensions to start
|
||||||
var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper != null).Select(w => w!).ToList();
|
var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper is not null).Select(w => w!).ToList();
|
||||||
|
|
||||||
lock (_commandProvidersLock)
|
lock (_commandProvidersLock)
|
||||||
{
|
{
|
||||||
@ -285,7 +293,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
|||||||
// Load the commands from the providers in parallel
|
// Load the commands from the providers in parallel
|
||||||
var loadTasks = wrappers.Select(LoadCommandsWithTimeoutAsync);
|
var loadTasks = wrappers.Select(LoadCommandsWithTimeoutAsync);
|
||||||
|
|
||||||
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results != null).Select(r => r!).ToList();
|
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results is not null).Select(r => r!).ToList();
|
||||||
|
|
||||||
lock (TopLevelCommands)
|
lock (TopLevelCommands)
|
||||||
{
|
{
|
||||||
@ -402,8 +410,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
|||||||
|
|
||||||
void IPageContext.ShowException(Exception ex, string? extensionHint)
|
void IPageContext.ShowException(Exception ex, string? extensionHint)
|
||||||
{
|
{
|
||||||
var errorMessage = $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n";
|
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager");
|
||||||
CommandPaletteHost.Instance.Log(errorMessage);
|
CommandPaletteHost.Instance.Log(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool IsProviderActive(string id)
|
internal bool IsProviderActive(string id)
|
||||||
|
@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
@ -239,7 +240,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
|||||||
private void FetchAliasFromAliasManager()
|
private void FetchAliasFromAliasManager()
|
||||||
{
|
{
|
||||||
var am = _serviceProvider.GetService<AliasManager>();
|
var am = _serviceProvider.GetService<AliasManager>();
|
||||||
if (am != null)
|
if (am is not null)
|
||||||
{
|
{
|
||||||
var commandAlias = am.AliasFromId(Id);
|
var commandAlias = am.AliasFromId(Id);
|
||||||
if (commandAlias is not null)
|
if (commandAlias is not null)
|
||||||
@ -253,7 +254,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
|||||||
private void UpdateHotkey()
|
private void UpdateHotkey()
|
||||||
{
|
{
|
||||||
var hotkey = _settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault();
|
var hotkey = _settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault();
|
||||||
if (hotkey != null)
|
if (hotkey is not null)
|
||||||
{
|
{
|
||||||
_hotkey = hotkey.Hotkey;
|
_hotkey = hotkey.Hotkey;
|
||||||
}
|
}
|
||||||
@ -263,12 +264,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
|||||||
{
|
{
|
||||||
List<Tag> tags = [];
|
List<Tag> tags = [];
|
||||||
|
|
||||||
if (Hotkey != null)
|
if (Hotkey is not null)
|
||||||
{
|
{
|
||||||
tags.Add(new Tag() { Text = Hotkey.ToString() });
|
tags.Add(new Tag() { Text = Hotkey.ToString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Alias != null)
|
if (Alias is not null)
|
||||||
{
|
{
|
||||||
tags.Add(new Tag() { Text = Alias.SearchPrefix });
|
tags.Add(new Tag() { Text = Alias.SearchPrefix });
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,9 @@ public partial class App : Application
|
|||||||
|
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
// Ensure types used in XAML are preserved for AOT compilation
|
||||||
|
TypePreservation.PreserveTypes();
|
||||||
|
|
||||||
NativeEventWaiter.WaitForEventLoop(
|
NativeEventWaiter.WaitForEventLoop(
|
||||||
"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd", () =>
|
"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd", () =>
|
||||||
{
|
{
|
||||||
|
@ -47,7 +47,8 @@
|
|||||||
<Flyout
|
<Flyout
|
||||||
x:Name="ContextMenuFlyout"
|
x:Name="ContextMenuFlyout"
|
||||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||||
Opened="ContextMenuFlyout_Opened">
|
Opened="ContextMenuFlyout_Opened"
|
||||||
|
ShouldConstrainToRootBounds="False">
|
||||||
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||||
</Flyout>
|
</Flyout>
|
||||||
|
|
||||||
@ -59,8 +60,24 @@
|
|||||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="6" />
|
<Setter Property="CornerRadius" Value="6" />
|
||||||
|
<Setter Property="MinWidth" Value="20" />
|
||||||
</Style.Setters>
|
</Style.Setters>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="HotkeyTextBlockStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="10" />
|
||||||
|
<Setter Property="CharacterSpacing" Value="4" />
|
||||||
|
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="HotkeyFontIconStyle" TargetType="FontIcon">
|
||||||
|
<Setter Property="FontSize" Value="10" />
|
||||||
|
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
@ -155,12 +172,7 @@
|
|||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}" />
|
Text="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}" />
|
||||||
<Border Style="{StaticResource HotkeyStyle}">
|
<Border Style="{StaticResource HotkeyStyle}">
|
||||||
<FontIcon
|
<FontIcon Glyph="" Style="{StaticResource HotkeyFontIconStyle}" />
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
|
||||||
Glyph="" />
|
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
@ -179,19 +191,10 @@
|
|||||||
Text="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}" />
|
Text="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||||
<TextBlock
|
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
|
||||||
CharacterSpacing="4"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
|
||||||
Text="Ctrl" />
|
|
||||||
</Border>
|
</Border>
|
||||||
<Border Style="{StaticResource HotkeyStyle}">
|
<Border Style="{StaticResource HotkeyStyle}">
|
||||||
<FontIcon
|
<FontIcon Glyph="" Style="{StaticResource HotkeyFontIconStyle}" />
|
||||||
HorizontalAlignment="Left"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
|
||||||
Glyph="" />
|
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@ -199,7 +202,7 @@
|
|||||||
<Button
|
<Button
|
||||||
x:Name="MoreCommandsButton"
|
x:Name="MoreCommandsButton"
|
||||||
x:Uid="MoreCommandsButton"
|
x:Uid="MoreCommandsButton"
|
||||||
Padding="4"
|
Padding="6,4,4,4"
|
||||||
Click="MoreCommandsButton_Clicked"
|
Click="MoreCommandsButton_Clicked"
|
||||||
Style="{StaticResource SubtleButtonStyle}"
|
Style="{StaticResource SubtleButtonStyle}"
|
||||||
ToolTipService.ToolTip="Ctrl+K"
|
ToolTipService.ToolTip="Ctrl+K"
|
||||||
@ -209,32 +212,12 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="More" />
|
Text="More" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
<Border
|
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||||
Padding="4,2,4,2"
|
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="Ctrl" />
|
||||||
VerticalAlignment="Center"
|
|
||||||
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
|
|
||||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="4">
|
|
||||||
<TextBlock
|
|
||||||
CharacterSpacing="4"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
Text="Ctrl" />
|
|
||||||
</Border>
|
</Border>
|
||||||
<Border
|
<Border Padding="4,2,4,2" Style="{StaticResource HotkeyStyle}">
|
||||||
Padding="4,2,4,2"
|
<TextBlock Style="{StaticResource HotkeyTextBlockStyle}" Text="K" />
|
||||||
VerticalAlignment="Center"
|
|
||||||
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
|
|
||||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="4">
|
|
||||||
<TextBlock
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
FontSize="10"
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
Text="K" />
|
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
@ -10,7 +10,6 @@ using Microsoft.CmdPal.UI.Views;
|
|||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||||
using Microsoft.UI.Xaml.Input;
|
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.Controls;
|
namespace Microsoft.CmdPal.UI.Controls;
|
||||||
@ -50,7 +49,7 @@ public sealed partial class CommandBar : UserControl,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.Element == null)
|
if (message.Element is null)
|
||||||
{
|
{
|
||||||
_ = DispatcherQueue.TryEnqueue(
|
_ = DispatcherQueue.TryEnqueue(
|
||||||
() =>
|
() =>
|
||||||
|
@ -44,7 +44,7 @@ public sealed partial class ContentFormControl : UserControl
|
|||||||
// 5% BODGY: if we set this multiple times over the lifetime of the app,
|
// 5% BODGY: if we set this multiple times over the lifetime of the app,
|
||||||
// then the second call will explode, because "CardOverrideStyles is already the child of another element".
|
// then the second call will explode, because "CardOverrideStyles is already the child of another element".
|
||||||
// SO only set this once.
|
// SO only set this once.
|
||||||
if (_renderer.OverrideStyles == null)
|
if (_renderer.OverrideStyles is null)
|
||||||
{
|
{
|
||||||
_renderer.OverrideStyles = CardOverrideStyles;
|
_renderer.OverrideStyles = CardOverrideStyles;
|
||||||
}
|
}
|
||||||
@ -55,19 +55,19 @@ public sealed partial class ContentFormControl : UserControl
|
|||||||
|
|
||||||
private void AttachViewModel(ContentFormViewModel? vm)
|
private void AttachViewModel(ContentFormViewModel? vm)
|
||||||
{
|
{
|
||||||
if (_viewModel != null)
|
if (_viewModel is not null)
|
||||||
{
|
{
|
||||||
_viewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
_viewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
_viewModel = vm;
|
_viewModel = vm;
|
||||||
|
|
||||||
if (_viewModel != null)
|
if (_viewModel is not null)
|
||||||
{
|
{
|
||||||
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
|
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||||
|
|
||||||
var c = _viewModel.Card;
|
var c = _viewModel.Card;
|
||||||
if (c != null)
|
if (c is not null)
|
||||||
{
|
{
|
||||||
DisplayCard(c);
|
DisplayCard(c);
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ public sealed partial class ContentFormControl : UserControl
|
|||||||
|
|
||||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (ViewModel == null)
|
if (ViewModel is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -84,7 +84,7 @@ public sealed partial class ContentFormControl : UserControl
|
|||||||
if (e.PropertyName == nameof(ViewModel.Card))
|
if (e.PropertyName == nameof(ViewModel.Card))
|
||||||
{
|
{
|
||||||
var c = ViewModel.Card;
|
var c = ViewModel.Card;
|
||||||
if (c != null)
|
if (c is not null)
|
||||||
{
|
{
|
||||||
DisplayCard(c);
|
DisplayCard(c);
|
||||||
}
|
}
|
||||||
@ -95,7 +95,7 @@ public sealed partial class ContentFormControl : UserControl
|
|||||||
{
|
{
|
||||||
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
|
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
|
||||||
ContentGrid.Children.Clear();
|
ContentGrid.Children.Clear();
|
||||||
if (_renderedCard.FrameworkElement != null)
|
if (_renderedCard.FrameworkElement is not null)
|
||||||
{
|
{
|
||||||
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
|
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ public sealed partial class ContentFormControl : UserControl
|
|||||||
|
|
||||||
// Recursively check children
|
// Recursively check children
|
||||||
var result = FindFirstFocusableElement(child);
|
var result = FindFirstFocusableElement(child);
|
||||||
if (result != null)
|
if (result is not null)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
@ -35,6 +36,17 @@ public partial class ContentIcon : FontIcon
|
|||||||
{
|
{
|
||||||
if (this.FindDescendants().OfType<Grid>().FirstOrDefault() is Grid grid && Content is not null)
|
if (this.FindDescendants().OfType<Grid>().FirstOrDefault() is Grid grid && Content is not null)
|
||||||
{
|
{
|
||||||
|
if (grid.Children.Contains(Content))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Content is FrameworkElement element && element.Parent is not null)
|
||||||
|
{
|
||||||
|
Debug.Assert(false, $"IconBoxElement Content is already parented to {element.Parent.GetType().Name}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
grid.Children.Add(Content);
|
grid.Children.Add(Content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@
|
|||||||
<Rectangle
|
<Rectangle
|
||||||
Height="1"
|
Height="1"
|
||||||
Margin="-16,-12,-12,-12"
|
Margin="-16,-12,-12,-12"
|
||||||
Fill="{ThemeResource MenuFlyoutSeparatorThemeBrush}" />
|
Fill="{ThemeResource MenuFlyoutSeparatorBackground}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
@ -31,7 +31,7 @@ public sealed partial class ContextMenu : UserControl,
|
|||||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||||
|
|
||||||
if (ViewModel != null)
|
if (ViewModel is not null)
|
||||||
{
|
{
|
||||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ public partial class IconBox : ContentControl
|
|||||||
{
|
{
|
||||||
if (d is IconBox @this)
|
if (d is IconBox @this)
|
||||||
{
|
{
|
||||||
if (e.NewValue == null)
|
if (e.NewValue is null)
|
||||||
{
|
{
|
||||||
@this.Source = null;
|
@this.Source = null;
|
||||||
}
|
}
|
||||||
@ -104,7 +104,7 @@ public partial class IconBox : ContentControl
|
|||||||
var requestedTheme = @this.ActualTheme;
|
var requestedTheme = @this.ActualTheme;
|
||||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||||
|
|
||||||
if (@this.SourceRequested != null)
|
if (@this.SourceRequested is not null)
|
||||||
{
|
{
|
||||||
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
|
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ public partial class IconBox : ContentControl
|
|||||||
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark;
|
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iconData != null &&
|
if (iconData is not null &&
|
||||||
@this.Source is FontIconSource)
|
@this.Source is FontIconSource)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2)
|
if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2)
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using Microsoft.CmdPal.UI;
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Markup;
|
using Microsoft.UI.Xaml.Markup;
|
||||||
@ -80,12 +79,12 @@ public sealed partial class KeyVisual : Control
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (_keyVisual == null)
|
if (_keyVisual is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_keyVisual.Content != null)
|
if (_keyVisual.Content is not null)
|
||||||
{
|
{
|
||||||
if (_keyVisual.Content.GetType() == typeof(string))
|
if (_keyVisual.Content.GetType() == typeof(string))
|
||||||
{
|
{
|
||||||
|
@ -51,13 +51,13 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
//// TODO: If the Debounce timer hasn't fired, we may want to store the current Filter in the OldValue/prior VM, but we don't want that to go actually do work...
|
//// TODO: If the Debounce timer hasn't fired, we may want to store the current Filter in the OldValue/prior VM, but we don't want that to go actually do work...
|
||||||
var @this = (SearchBar)d;
|
var @this = (SearchBar)d;
|
||||||
|
|
||||||
if (@this != null
|
if (@this is not null
|
||||||
&& e.OldValue is PageViewModel old)
|
&& e.OldValue is PageViewModel old)
|
||||||
{
|
{
|
||||||
old.PropertyChanged -= @this.Page_PropertyChanged;
|
old.PropertyChanged -= @this.Page_PropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@this != null
|
if (@this is not null
|
||||||
&& e.NewValue is PageViewModel page)
|
&& e.NewValue is PageViewModel page)
|
||||||
{
|
{
|
||||||
// TODO: In some cases we probably want commands to clear a filter
|
// TODO: In some cases we probably want commands to clear a filter
|
||||||
@ -85,7 +85,7 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
{
|
{
|
||||||
this.FilterBox.Text = string.Empty;
|
this.FilterBox.Text = string.Empty;
|
||||||
|
|
||||||
if (CurrentPageViewModel != null)
|
if (CurrentPageViewModel is not null)
|
||||||
{
|
{
|
||||||
CurrentPageViewModel.Filter = string.Empty;
|
CurrentPageViewModel.Filter = string.Empty;
|
||||||
}
|
}
|
||||||
@ -143,7 +143,7 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
FilterBox.Text = string.Empty;
|
FilterBox.Text = string.Empty;
|
||||||
|
|
||||||
// hack TODO GH #245
|
// hack TODO GH #245
|
||||||
if (CurrentPageViewModel != null)
|
if (CurrentPageViewModel is not null)
|
||||||
{
|
{
|
||||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||||
}
|
}
|
||||||
@ -154,7 +154,7 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
else if (e.Key == VirtualKey.Back)
|
else if (e.Key == VirtualKey.Back)
|
||||||
{
|
{
|
||||||
// hack TODO GH #245
|
// hack TODO GH #245
|
||||||
if (CurrentPageViewModel != null)
|
if (CurrentPageViewModel is not null)
|
||||||
{
|
{
|
||||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||||
}
|
}
|
||||||
@ -318,7 +318,7 @@ public sealed partial class SearchBar : UserControl,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Actually plumb Filtering to the view model
|
// Actually plumb Filtering to the view model
|
||||||
if (CurrentPageViewModel != null)
|
if (CurrentPageViewModel is not null)
|
||||||
{
|
{
|
||||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||||
}
|
}
|
||||||
|
@ -39,13 +39,13 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
private static void OnAllowDisableChanged(DependencyObject d, DependencyPropertyChangedEventArgs? e)
|
private static void OnAllowDisableChanged(DependencyObject d, DependencyPropertyChangedEventArgs? e)
|
||||||
{
|
{
|
||||||
var me = d as ShortcutControl;
|
var me = d as ShortcutControl;
|
||||||
if (me == null)
|
if (me is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var description = me.c?.FindDescendant<TextBlock>();
|
var description = me.c?.FindDescendant<TextBlock>();
|
||||||
if (description == null)
|
if (description is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -431,7 +431,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
|
|
||||||
private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
{
|
{
|
||||||
if (lastValidSettings != null && ComboIsValid(lastValidSettings))
|
if (lastValidSettings is not null && ComboIsValid(lastValidSettings))
|
||||||
{
|
{
|
||||||
HotkeySettings = lastValidSettings with { };
|
HotkeySettings = lastValidSettings with { };
|
||||||
}
|
}
|
||||||
@ -458,7 +458,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
|
|
||||||
private static bool ComboIsValid(HotkeySettings? settings)
|
private static bool ComboIsValid(HotkeySettings? settings)
|
||||||
{
|
{
|
||||||
return settings != null && (settings.IsValid() || settings.IsEmpty());
|
return settings is not null && (settings.IsValid() || settings.IsEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(WindowActivatedEventArgs message) => DoWindowActivated(message);
|
public void Receive(WindowActivatedEventArgs message) => DoWindowActivated(message);
|
||||||
@ -466,12 +466,12 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
private void DoWindowActivated(WindowActivatedEventArgs args)
|
private void DoWindowActivated(WindowActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
if (args.WindowActivationState != WindowActivationState.Deactivated && (hook == null || hook.GetDisposedState() == true))
|
if (args.WindowActivationState != WindowActivationState.Deactivated && (hook is null || hook.GetDisposedState() == true))
|
||||||
{
|
{
|
||||||
// If the PT settings window gets focussed/activated again, we enable the keyboard hook to catch the keyboard input.
|
// If the PT settings window gets focussed/activated again, we enable the keyboard hook to catch the keyboard input.
|
||||||
hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents);
|
hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents);
|
||||||
}
|
}
|
||||||
else if (args.WindowActivationState == WindowActivationState.Deactivated && hook != null && hook.GetDisposedState() == false)
|
else if (args.WindowActivationState == WindowActivationState.Deactivated && hook is not null && hook.GetDisposedState() == false)
|
||||||
{
|
{
|
||||||
// If the PT settings window lost focus/activation, we disable the keyboard hook to allow keyboard input on other windows.
|
// If the PT settings window lost focus/activation, we disable the keyboard hook to allow keyboard input on other windows.
|
||||||
hook.Dispose();
|
hook.Dispose();
|
||||||
@ -490,7 +490,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
if (hook != null)
|
if (hook is not null)
|
||||||
{
|
{
|
||||||
hook.Dispose();
|
hook.Dispose();
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" />
|
<TextBlock Grid.Row="0" IsTabStop="True" />
|
||||||
|
|
||||||
<ItemsControl
|
<ItemsControl
|
||||||
x:Name="KeysControl"
|
x:Name="KeysControl"
|
||||||
|
@ -84,7 +84,7 @@ public partial class Tag : Control
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag.ForegroundColor != null &&
|
if (tag.ForegroundColor is not null &&
|
||||||
OptionalColorBrushCacheProvider.Convert(tag.ForegroundColor.Value) is SolidColorBrush brush)
|
OptionalColorBrushCacheProvider.Convert(tag.ForegroundColor.Value) is SolidColorBrush brush)
|
||||||
{
|
{
|
||||||
tag.Foreground = brush;
|
tag.Foreground = brush;
|
||||||
@ -114,7 +114,7 @@ public partial class Tag : Control
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag.BackgroundColor != null &&
|
if (tag.BackgroundColor is not null &&
|
||||||
OptionalColorBrushCacheProvider.Convert(tag.BackgroundColor.Value) is SolidColorBrush brush)
|
OptionalColorBrushCacheProvider.Convert(tag.BackgroundColor.Value) is SolidColorBrush brush)
|
||||||
{
|
{
|
||||||
tag.Background = brush;
|
tag.Background = brush;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
@ -10,6 +11,7 @@ using Microsoft.UI.Xaml.Data;
|
|||||||
|
|
||||||
namespace Microsoft.CmdPal.UI;
|
namespace Microsoft.CmdPal.UI;
|
||||||
|
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||||
internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
|
internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
|
||||||
{
|
{
|
||||||
public DataTemplate? Default { get; set; }
|
public DataTemplate? Default { get; set; }
|
||||||
|
@ -34,8 +34,14 @@ public sealed partial class ContentPage : Page,
|
|||||||
public ContentPage()
|
public ContentPage()
|
||||||
{
|
{
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
this.Unloaded += OnUnloaded;
|
||||||
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
|
}
|
||||||
|
|
||||||
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Unhook from everything to ensure nothing can reach us
|
||||||
|
// between this point and our complete and utter destruction.
|
||||||
|
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||||
@ -45,6 +51,16 @@ public sealed partial class ContentPage : Page,
|
|||||||
ViewModel = vm;
|
ViewModel = vm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!WeakReferenceMessenger.Default.IsRegistered<ActivateSelectedListItemMessage>(this))
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WeakReferenceMessenger.Default.IsRegistered<ActivateSecondaryCommandMessage>(this))
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
|
||||||
|
}
|
||||||
|
|
||||||
base.OnNavigatedTo(e);
|
base.OnNavigatedTo(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ using Microsoft.UI.Xaml.Controls;
|
|||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using Microsoft.UI.Xaml.Navigation;
|
using Microsoft.UI.Xaml.Navigation;
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI;
|
namespace Microsoft.CmdPal.UI;
|
||||||
|
|
||||||
@ -24,6 +25,8 @@ public sealed partial class ListPage : Page,
|
|||||||
IRecipient<ActivateSelectedListItemMessage>,
|
IRecipient<ActivateSelectedListItemMessage>,
|
||||||
IRecipient<ActivateSecondaryCommandMessage>
|
IRecipient<ActivateSecondaryCommandMessage>
|
||||||
{
|
{
|
||||||
|
private InputSource _lastInputSource;
|
||||||
|
|
||||||
private ListViewModel? ViewModel
|
private ListViewModel? ViewModel
|
||||||
{
|
{
|
||||||
get => (ListViewModel?)GetValue(ViewModelProperty);
|
get => (ListViewModel?)GetValue(ViewModelProperty);
|
||||||
@ -39,6 +42,8 @@ public sealed partial class ListPage : Page,
|
|||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
this.NavigationCacheMode = NavigationCacheMode.Disabled;
|
this.NavigationCacheMode = NavigationCacheMode.Disabled;
|
||||||
this.ItemsList.Loaded += ItemsList_Loaded;
|
this.ItemsList.Loaded += ItemsList_Loaded;
|
||||||
|
this.ItemsList.PreviewKeyDown += ItemsList_PreviewKeyDown;
|
||||||
|
this.ItemsList.PointerPressed += ItemsList_PointerPressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||||
@ -74,7 +79,7 @@ public sealed partial class ListPage : Page,
|
|||||||
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
|
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
|
||||||
|
|
||||||
if (ViewModel != null)
|
if (ViewModel is not null)
|
||||||
{
|
{
|
||||||
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
||||||
ViewModel.ItemsUpdated -= Page_ItemsUpdated;
|
ViewModel.ItemsUpdated -= Page_ItemsUpdated;
|
||||||
@ -98,6 +103,12 @@ public sealed partial class ListPage : Page,
|
|||||||
{
|
{
|
||||||
if (e.ClickedItem is ListItemViewModel item)
|
if (e.ClickedItem is ListItemViewModel item)
|
||||||
{
|
{
|
||||||
|
if (_lastInputSource == InputSource.Keyboard)
|
||||||
|
{
|
||||||
|
ViewModel?.InvokeItemCommand.Execute(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||||
if (settings.SingleClickActivates)
|
if (settings.SingleClickActivates)
|
||||||
{
|
{
|
||||||
@ -142,13 +153,13 @@ public sealed partial class ListPage : Page,
|
|||||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||||
// it's in the list (otherwise, clear the cache), but that seems
|
// it's in the list (otherwise, clear the cache), but that seems
|
||||||
// aggressively BODGY for something that mostly just works today.
|
// aggressively BODGY for something that mostly just works today.
|
||||||
if (ItemsList.SelectedItem != null)
|
if (ItemsList.SelectedItem is not null)
|
||||||
{
|
{
|
||||||
ItemsList.ScrollIntoView(ItemsList.SelectedItem);
|
ItemsList.ScrollIntoView(ItemsList.SelectedItem);
|
||||||
|
|
||||||
// Automation notification for screen readers
|
// Automation notification for screen readers
|
||||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemsList);
|
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemsList);
|
||||||
if (listViewPeer != null && li != null)
|
if (listViewPeer is not null && li is not null)
|
||||||
{
|
{
|
||||||
var notificationText = li.Title;
|
var notificationText = li.Title;
|
||||||
listViewPeer.RaiseNotificationEvent(
|
listViewPeer.RaiseNotificationEvent(
|
||||||
@ -165,7 +176,7 @@ public sealed partial class ListPage : Page,
|
|||||||
// Find the ScrollViewer in the ListView
|
// Find the ScrollViewer in the ListView
|
||||||
var listViewScrollViewer = FindScrollViewer(this.ItemsList);
|
var listViewScrollViewer = FindScrollViewer(this.ItemsList);
|
||||||
|
|
||||||
if (listViewScrollViewer != null)
|
if (listViewScrollViewer is not null)
|
||||||
{
|
{
|
||||||
listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
|
listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
|
||||||
}
|
}
|
||||||
@ -174,7 +185,7 @@ public sealed partial class ListPage : Page,
|
|||||||
private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
|
private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
|
||||||
{
|
{
|
||||||
var scrollView = sender as ScrollViewer;
|
var scrollView = sender as ScrollViewer;
|
||||||
if (scrollView == null)
|
if (scrollView is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -256,7 +267,7 @@ public sealed partial class ListPage : Page,
|
|||||||
page.PropertyChanged += @this.ViewModel_PropertyChanged;
|
page.PropertyChanged += @this.ViewModel_PropertyChanged;
|
||||||
page.ItemsUpdated += @this.Page_ItemsUpdated;
|
page.ItemsUpdated += @this.Page_ItemsUpdated;
|
||||||
}
|
}
|
||||||
else if (e.NewValue == null)
|
else if (e.NewValue is null)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("cleared view model");
|
Logger.LogDebug("cleared view model");
|
||||||
}
|
}
|
||||||
@ -274,7 +285,7 @@ public sealed partial class ListPage : Page,
|
|||||||
// ItemsList_SelectionChanged again to give us another chance to change
|
// ItemsList_SelectionChanged again to give us another chance to change
|
||||||
// the selection from null -> something. Better to just update the
|
// the selection from null -> something. Better to just update the
|
||||||
// selection once, at the end of all the updating.
|
// selection once, at the end of all the updating.
|
||||||
if (ItemsList.SelectedItem == null)
|
if (ItemsList.SelectedItem is null)
|
||||||
{
|
{
|
||||||
ItemsList.SelectedIndex = 0;
|
ItemsList.SelectedIndex = 0;
|
||||||
}
|
}
|
||||||
@ -307,7 +318,7 @@ public sealed partial class ListPage : Page,
|
|||||||
{
|
{
|
||||||
var child = VisualTreeHelper.GetChild(parent, i);
|
var child = VisualTreeHelper.GetChild(parent, i);
|
||||||
var result = FindScrollViewer(child);
|
var result = FindScrollViewer(child);
|
||||||
if (result != null)
|
if (result is not null)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -329,7 +340,7 @@ public sealed partial class ListPage : Page,
|
|||||||
_ => (null, null),
|
_ => (null, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (item == null || element == null)
|
if (item is null || element is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -363,4 +374,21 @@ public sealed partial class ListPage : Page,
|
|||||||
{
|
{
|
||||||
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
|
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ItemsList_PointerPressed(object sender, PointerRoutedEventArgs e) => _lastInputSource = InputSource.Pointer;
|
||||||
|
|
||||||
|
private void ItemsList_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key is VirtualKey.Enter or VirtualKey.Space)
|
||||||
|
{
|
||||||
|
_lastInputSource = InputSource.Keyboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum InputSource
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Keyboard,
|
||||||
|
Pointer,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,6 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// 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.CmdPal.Ext.Bookmarks;
|
|
||||||
using Microsoft.UI.Xaml.Documents;
|
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.Helpers;
|
namespace Microsoft.CmdPal.UI.Helpers;
|
||||||
@ -63,7 +56,7 @@ internal static class GpoValueChecker
|
|||||||
{
|
{
|
||||||
using (RegistryKey? key = rootKey.OpenSubKey(subKeyPath, false))
|
using (RegistryKey? key = rootKey.OpenSubKey(subKeyPath, false))
|
||||||
{
|
{
|
||||||
if (key == null)
|
if (key is null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.UI.Controls;
|
using Microsoft.CmdPal.UI.Controls;
|
||||||
using Microsoft.CmdPal.UI.Helpers;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.Helpers;
|
namespace Microsoft.CmdPal.UI.Helpers;
|
||||||
|
|
||||||
@ -19,7 +18,7 @@ public static partial class IconCacheProvider
|
|||||||
public static async void SourceRequested(IconBox sender, SourceRequestedEventArgs args)
|
public static async void SourceRequested(IconBox sender, SourceRequestedEventArgs args)
|
||||||
#pragma warning restore IDE0060 // Remove unused parameter
|
#pragma warning restore IDE0060 // Remove unused parameter
|
||||||
{
|
{
|
||||||
if (args.Key == null)
|
if (args.Key is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
|||||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false);
|
var source = IconPathConverter.IconSourceMUX(icon.Icon, false);
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
else if (icon.Data != null)
|
else if (icon.Data is not null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -49,7 +49,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
|||||||
|
|
||||||
private async Task<IconSource?> StreamToIconSource(IRandomAccessStreamReference iconStreamRef)
|
private async Task<IconSource?> StreamToIconSource(IRandomAccessStreamReference iconStreamRef)
|
||||||
{
|
{
|
||||||
if (iconStreamRef == null)
|
if (iconStreamRef is null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,157 @@
|
|||||||
|
// 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 ManagedCommon;
|
||||||
|
|
||||||
|
using Windows.System;
|
||||||
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.UI.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class that listens for local keyboard events using a Windows hook.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LocalKeyboardListener : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is raised when a key is pressed down.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<LocalKeyboardListenerKeyPressedEventArgs>? KeyPressed;
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
private UnhookWindowsHookExSafeHandle? _handle;
|
||||||
|
private HOOKPROC? _hookProc; // Keep reference to prevent GC collection
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a global keyboard hook to listen for key down events.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// Throws if the hook could not be registered, which may happen if the system is unable to set the hook.
|
||||||
|
/// </exception>
|
||||||
|
public void RegisterKeyboardHook()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
if (_handle is not null && !_handle.IsInvalid)
|
||||||
|
{
|
||||||
|
// Hook is already set
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hookProc = KeyEventHook;
|
||||||
|
if (!SetWindowKeyHook(_hookProc))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to register keyboard hook.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to register a global keyboard hook to listen for key down events.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true"/> if the keyboard hook was successfully registered; otherwise, <see langword="false"/>.
|
||||||
|
/// </returns>
|
||||||
|
public bool Start()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RegisterKeyboardHook();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("Failed to register hook", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnregisterKeyboardHook()
|
||||||
|
{
|
||||||
|
if (_handle is not null && !_handle.IsInvalid)
|
||||||
|
{
|
||||||
|
// The SafeHandle should automatically call UnhookWindowsHookEx when disposed
|
||||||
|
_handle.Dispose();
|
||||||
|
_handle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hookProc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SetWindowKeyHook(HOOKPROC hookProc)
|
||||||
|
{
|
||||||
|
if (_handle is not null && !_handle.IsInvalid)
|
||||||
|
{
|
||||||
|
// Hook is already set
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handle = PInvoke.SetWindowsHookEx(
|
||||||
|
WINDOWS_HOOK_ID.WH_KEYBOARD,
|
||||||
|
hookProc,
|
||||||
|
PInvoke.GetModuleHandle(null),
|
||||||
|
PInvoke.GetCurrentThreadId());
|
||||||
|
|
||||||
|
// Check if the hook was successfully set
|
||||||
|
return _handle is not null && !_handle.IsInvalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsKeyDownHook(LPARAM lParam)
|
||||||
|
{
|
||||||
|
// The 30th bit tells what the previous key state is with 0 being the "UP" state
|
||||||
|
// For more info see https://learn.microsoft.com/windows/win32/winmsg/keyboardproc#lparam-in
|
||||||
|
return ((lParam.Value >> 30) & 1) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LRESULT KeyEventHook(int nCode, WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (nCode >= 0 && IsKeyDownHook(lParam))
|
||||||
|
{
|
||||||
|
InvokeKeyDown((VirtualKey)wParam.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("Failed when invoking key down keyboard hook event", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call next hook in chain - pass null as first parameter for current hook
|
||||||
|
return PInvoke.CallNextHookEx(null, nCode, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvokeKeyDown(VirtualKey virtualKey)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
KeyPressed?.Invoke(this, new LocalKeyboardListenerKeyPressedEventArgs(virtualKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
UnregisterKeyboardHook();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.UI.Helpers;
|
||||||
|
|
||||||
|
public class LocalKeyboardListenerKeyPressedEventArgs(VirtualKey key) : EventArgs
|
||||||
|
{
|
||||||
|
public VirtualKey Key { get; } = key;
|
||||||
|
}
|
@ -5,8 +5,9 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.UI.Messages;
|
||||||
using Microsoft.CmdPal.UI.ViewModels;
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
@ -49,7 +50,7 @@ internal sealed partial class TrayIconService
|
|||||||
{
|
{
|
||||||
if (showSystemTrayIcon ?? _settingsModel.ShowSystemTrayIcon)
|
if (showSystemTrayIcon ?? _settingsModel.ShowSystemTrayIcon)
|
||||||
{
|
{
|
||||||
if (_window == null)
|
if (_window is null)
|
||||||
{
|
{
|
||||||
_window = new Window();
|
_window = new Window();
|
||||||
_hwnd = new HWND(WindowNative.GetWindowHandle(_window));
|
_hwnd = new HWND(WindowNative.GetWindowHandle(_window));
|
||||||
@ -63,7 +64,7 @@ internal sealed partial class TrayIconService
|
|||||||
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
|
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_trayIconData == null)
|
if (_trayIconData is null)
|
||||||
{
|
{
|
||||||
// We need to stash this handle, so it doesn't clean itself up. If
|
// We need to stash this handle, so it doesn't clean itself up. If
|
||||||
// explorer restarts, we'll come back through here, and we don't
|
// explorer restarts, we'll come back through here, and we don't
|
||||||
@ -87,7 +88,7 @@ internal sealed partial class TrayIconService
|
|||||||
// Add the notification icon
|
// Add the notification icon
|
||||||
PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d);
|
PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d);
|
||||||
|
|
||||||
if (_popupMenu == null)
|
if (_popupMenu is null)
|
||||||
{
|
{
|
||||||
_popupMenu = PInvoke.CreatePopupMenu_SafeHandle();
|
_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, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 1, RS_.GetString("TrayMenu_Settings"));
|
||||||
@ -102,7 +103,7 @@ internal sealed partial class TrayIconService
|
|||||||
|
|
||||||
public void Destroy()
|
public void Destroy()
|
||||||
{
|
{
|
||||||
if (_trayIconData != null)
|
if (_trayIconData is not null)
|
||||||
{
|
{
|
||||||
var d = (NOTIFYICONDATAW)_trayIconData;
|
var d = (NOTIFYICONDATAW)_trayIconData;
|
||||||
if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d))
|
if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d))
|
||||||
@ -111,19 +112,19 @@ internal sealed partial class TrayIconService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_popupMenu != null)
|
if (_popupMenu is not null)
|
||||||
{
|
{
|
||||||
_popupMenu.Close();
|
_popupMenu.Close();
|
||||||
_popupMenu = null;
|
_popupMenu = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_largeIcon != null)
|
if (_largeIcon is not null)
|
||||||
{
|
{
|
||||||
_largeIcon.Close();
|
_largeIcon.Close();
|
||||||
_largeIcon = null;
|
_largeIcon = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_window != null)
|
if (_window is not null)
|
||||||
{
|
{
|
||||||
_window.Close();
|
_window.Close();
|
||||||
_window = null;
|
_window = null;
|
||||||
@ -166,7 +167,7 @@ internal sealed partial class TrayIconService
|
|||||||
// WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
|
// WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
|
||||||
case PInvoke.WM_WINDOWPOSCHANGING:
|
case PInvoke.WM_WINDOWPOSCHANGING:
|
||||||
{
|
{
|
||||||
if (_trayIconData == null)
|
if (_trayIconData is null)
|
||||||
{
|
{
|
||||||
SetupTrayIcon();
|
SetupTrayIcon();
|
||||||
}
|
}
|
||||||
@ -188,7 +189,7 @@ internal sealed partial class TrayIconService
|
|||||||
{
|
{
|
||||||
case PInvoke.WM_RBUTTONUP:
|
case PInvoke.WM_RBUTTONUP:
|
||||||
{
|
{
|
||||||
if (_popupMenu != null)
|
if (_popupMenu is not null)
|
||||||
{
|
{
|
||||||
PInvoke.GetCursorPos(out var cursorPos);
|
PInvoke.GetCursorPos(out var cursorPos);
|
||||||
PInvoke.SetForegroundWindow(_hwnd);
|
PInvoke.SetForegroundWindow(_hwnd);
|
||||||
|
@ -46,7 +46,7 @@ public static class TypedEventHandlerExtensions
|
|||||||
#pragma warning restore CA1715 // Identifiers should have correct prefix
|
#pragma warning restore CA1715 // Identifiers should have correct prefix
|
||||||
where R : DeferredEventArgs
|
where R : DeferredEventArgs
|
||||||
{
|
{
|
||||||
if (eventHandler == null)
|
if (eventHandler is null)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ internal sealed partial class WindowHelper
|
|||||||
UserNotificationState state;
|
UserNotificationState state;
|
||||||
|
|
||||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
|
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
|
||||||
if (Marshal.GetExceptionForHR(NativeMethods.SHQueryUserNotificationState(out state)) == null)
|
if (Marshal.GetExceptionForHR(NativeMethods.SHQueryUserNotificationState(out state)) is null)
|
||||||
{
|
{
|
||||||
if (state == UserNotificationState.QUNS_RUNNING_D3D_FULL_SCREEN ||
|
if (state == UserNotificationState.QUNS_RUNNING_D3D_FULL_SCREEN ||
|
||||||
state == UserNotificationState.QUNS_BUSY ||
|
state == UserNotificationState.QUNS_BUSY ||
|
||||||
|
@ -10,11 +10,12 @@ using ManagedCommon;
|
|||||||
using Microsoft.CmdPal.Common.Helpers;
|
using Microsoft.CmdPal.Common.Helpers;
|
||||||
using Microsoft.CmdPal.Common.Messages;
|
using Microsoft.CmdPal.Common.Messages;
|
||||||
using Microsoft.CmdPal.Common.Services;
|
using Microsoft.CmdPal.Common.Services;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.Events;
|
using Microsoft.CmdPal.UI.Events;
|
||||||
using Microsoft.CmdPal.UI.Helpers;
|
using Microsoft.CmdPal.UI.Helpers;
|
||||||
|
using Microsoft.CmdPal.UI.Messages;
|
||||||
using Microsoft.CmdPal.UI.ViewModels;
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
using Microsoft.UI.Composition;
|
using Microsoft.UI.Composition;
|
||||||
@ -26,6 +27,7 @@ using Microsoft.Windows.AppLifecycle;
|
|||||||
using Windows.ApplicationModel.Activation;
|
using Windows.ApplicationModel.Activation;
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.Graphics;
|
using Windows.Graphics;
|
||||||
|
using Windows.System;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
using Windows.UI.WindowManagement;
|
using Windows.UI.WindowManagement;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
@ -43,7 +45,8 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
IRecipient<DismissMessage>,
|
IRecipient<DismissMessage>,
|
||||||
IRecipient<ShowWindowMessage>,
|
IRecipient<ShowWindowMessage>,
|
||||||
IRecipient<HideWindowMessage>,
|
IRecipient<HideWindowMessage>,
|
||||||
IRecipient<QuitMessage>
|
IRecipient<QuitMessage>,
|
||||||
|
IDisposable
|
||||||
{
|
{
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
||||||
@ -53,6 +56,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
private readonly WNDPROC? _originalWndProc;
|
private readonly WNDPROC? _originalWndProc;
|
||||||
private readonly List<TopLevelHotkey> _hotkeys = [];
|
private readonly List<TopLevelHotkey> _hotkeys = [];
|
||||||
private readonly KeyboardListener _keyboardListener;
|
private readonly KeyboardListener _keyboardListener;
|
||||||
|
private readonly LocalKeyboardListener _localKeyboardListener;
|
||||||
private bool _ignoreHotKeyWhenFullScreen = true;
|
private bool _ignoreHotKeyWhenFullScreen = true;
|
||||||
|
|
||||||
private DesktopAcrylicController? _acrylicController;
|
private DesktopAcrylicController? _acrylicController;
|
||||||
@ -115,6 +119,18 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
{
|
{
|
||||||
Summon(string.Empty);
|
Summon(string.Empty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_localKeyboardListener = new LocalKeyboardListener();
|
||||||
|
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
|
||||||
|
_localKeyboardListener.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == VirtualKey.GoBack)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new GoBackMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
|
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
|
||||||
@ -375,6 +391,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
|
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
|
||||||
// Workaround by turning it off before shutdown.
|
// Workaround by turning it off before shutdown.
|
||||||
App.Current.DebugSettings.FailFastOnErrors = false;
|
App.Current.DebugSettings.FailFastOnErrors = false;
|
||||||
|
_localKeyboardListener.Dispose();
|
||||||
DisposeAcrylic();
|
DisposeAcrylic();
|
||||||
|
|
||||||
_keyboardListener.Stop();
|
_keyboardListener.Stop();
|
||||||
@ -383,7 +400,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
private void DisposeAcrylic()
|
private void DisposeAcrylic()
|
||||||
{
|
{
|
||||||
if (_acrylicController != null)
|
if (_acrylicController is not null)
|
||||||
{
|
{
|
||||||
_acrylicController.Dispose();
|
_acrylicController.Dispose();
|
||||||
_acrylicController = null!;
|
_acrylicController = null!;
|
||||||
@ -458,7 +475,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
|
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_configurationSource != null)
|
if (_configurationSource is not null)
|
||||||
{
|
{
|
||||||
_configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
|
_configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
|
||||||
}
|
}
|
||||||
@ -466,7 +483,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
|
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
|
||||||
{
|
{
|
||||||
if (activatedEventArgs == null)
|
if (activatedEventArgs is null)
|
||||||
{
|
{
|
||||||
Summon(string.Empty);
|
Summon(string.Empty);
|
||||||
return;
|
return;
|
||||||
@ -534,7 +551,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
UnregisterHotkeys();
|
UnregisterHotkeys();
|
||||||
|
|
||||||
var globalHotkey = settings.Hotkey;
|
var globalHotkey = settings.Hotkey;
|
||||||
if (globalHotkey != null)
|
if (globalHotkey is not null)
|
||||||
{
|
{
|
||||||
if (settings.UseLowLevelGlobalHotkey)
|
if (settings.UseLowLevelGlobalHotkey)
|
||||||
{
|
{
|
||||||
@ -564,7 +581,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
{
|
{
|
||||||
var key = commandHotkey.Hotkey;
|
var key = commandHotkey.Hotkey;
|
||||||
|
|
||||||
if (key != null)
|
if (key is not null)
|
||||||
{
|
{
|
||||||
if (settings.UseLowLevelGlobalHotkey)
|
if (settings.UseLowLevelGlobalHotkey)
|
||||||
{
|
{
|
||||||
@ -681,4 +698,10 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
|
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_localKeyboardListener.Dispose();
|
||||||
|
DisposeAcrylic();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
namespace Microsoft.CmdPal.UI.Messages;
|
||||||
|
|
||||||
public record HotkeySummonMessage(string CommandId, IntPtr Hwnd)
|
public record HotkeySummonMessage(string CommandId, IntPtr Hwnd)
|
||||||
{
|
{
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
namespace Microsoft.CmdPal.UI.Messages;
|
||||||
|
|
||||||
public record SettingsWindowClosedMessage
|
public record SettingsWindowClosedMessage
|
||||||
{
|
{
|
@ -109,10 +109,6 @@
|
|||||||
<ProjectCapability Include="Msix" />
|
<ProjectCapability Include="Msix" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<RdXmlFile Include="rd.xml" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
|
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
|
||||||
|
@ -48,3 +48,9 @@ DWM_CLOAKED_APP
|
|||||||
CoWaitForMultipleObjects
|
CoWaitForMultipleObjects
|
||||||
INFINITE
|
INFINITE
|
||||||
CWMO_FLAGS
|
CWMO_FLAGS
|
||||||
|
|
||||||
|
GetCurrentThreadId
|
||||||
|
SetWindowsHookEx
|
||||||
|
UnhookWindowsHookEx
|
||||||
|
CallNextHookEx
|
||||||
|
GetModuleHandle
|
@ -24,7 +24,7 @@ public sealed partial class LoadingPage : Page
|
|||||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Parameter is ShellViewModel shellVM
|
if (e.Parameter is ShellViewModel shellVM
|
||||||
&& shellVM.LoadCommand != null)
|
&& shellVM.LoadCommand is not null)
|
||||||
{
|
{
|
||||||
// This will load the built-in commands, then navigate to the main page.
|
// This will load the built-in commands, then navigate to the main page.
|
||||||
// Once the mainpage loads, we'll start loading extensions.
|
// Once the mainpage loads, we'll start loading extensions.
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user