mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-07 12:57:03 +01:00
Compare commits
30 Commits
leilzh/tes
...
yuleng/ut/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83908c6cc1 | ||
|
|
1e517f2721 | ||
|
|
df08d98a81 | ||
|
|
44d34e45c0 | ||
|
|
3c0af323bf | ||
|
|
e0097c94c6 | ||
|
|
e1086726ec | ||
|
|
759f5c02cb | ||
|
|
e0428eef1d | ||
|
|
3bc746d0ff | ||
|
|
7472f75d75 | ||
|
|
75526b9580 | ||
|
|
ce4d8dc11e | ||
|
|
917da2e07e | ||
|
|
ea6115f892 | ||
|
|
8dbff245d6 | ||
|
|
fa741470bc | ||
|
|
446d8087a3 | ||
|
|
a0a8ce9f69 | ||
|
|
7b06fb3bdb | ||
|
|
6130d2ad39 | ||
|
|
8737de29af | ||
|
|
2f6876b85f | ||
|
|
8f93d0269f | ||
|
|
d2a4c96e12 | ||
|
|
409ae3d73a | ||
|
|
65b752b3ff | ||
|
|
6acb793184 | ||
|
|
efb48aa163 | ||
|
|
e8754e4cd6 |
47
.github/actions/spell-check/expect.txt
vendored
47
.github/actions/spell-check/expect.txt
vendored
@@ -25,11 +25,14 @@ ADMINS
|
||||
adml
|
||||
admx
|
||||
advancedpaste
|
||||
advancedpasteui
|
||||
advancedpasteuishortcut
|
||||
advfirewall
|
||||
AFeature
|
||||
affordances
|
||||
AFX
|
||||
AGGREGATABLE
|
||||
AHK
|
||||
AHybrid
|
||||
akv
|
||||
ALarger
|
||||
@@ -40,6 +43,7 @@ ALLINPUT
|
||||
Allman
|
||||
Allmodule
|
||||
ALLOWUNDO
|
||||
allpc
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
AModifier
|
||||
@@ -115,6 +119,7 @@ bigbar
|
||||
bigobj
|
||||
binlog
|
||||
binres
|
||||
binskim
|
||||
BITMAPFILEHEADER
|
||||
bitmapimage
|
||||
BITMAPINFO
|
||||
@@ -255,6 +260,7 @@ Corpor
|
||||
cotaskmem
|
||||
COULDNOT
|
||||
countof
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
cplusplus
|
||||
@@ -627,6 +633,7 @@ HKCU
|
||||
hkey
|
||||
HKLM
|
||||
HKM
|
||||
hkmng
|
||||
HKPD
|
||||
HKU
|
||||
HMD
|
||||
@@ -635,6 +642,7 @@ hmodule
|
||||
hmonitor
|
||||
homies
|
||||
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
||||
HOOKPROC
|
||||
HORZRES
|
||||
HORZSIZE
|
||||
Hostbackdropbrush
|
||||
@@ -643,7 +651,11 @@ Hostx
|
||||
hotfixes
|
||||
hotkeycontrol
|
||||
HOTKEYF
|
||||
hotkeylockmachine
|
||||
hotkeyreconnect
|
||||
hotkeys
|
||||
hotkeyswitch
|
||||
hotkeytoggleeasymouse
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
@@ -656,6 +668,7 @@ HROW
|
||||
hsb
|
||||
HSCROLL
|
||||
hsi
|
||||
HSpeed
|
||||
HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
@@ -701,9 +714,12 @@ IMAGERESIZERCONTEXTMENU
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
imagetotext
|
||||
imagetotextshortcut
|
||||
imagingdevices
|
||||
ime
|
||||
imgflip
|
||||
inapp
|
||||
inbox
|
||||
INCONTACT
|
||||
Indo
|
||||
@@ -757,6 +773,7 @@ istep
|
||||
ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
IUWP
|
||||
IWIC
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
@@ -786,6 +803,7 @@ keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
kvp
|
||||
Kybd
|
||||
lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
@@ -824,6 +842,7 @@ localappdata
|
||||
localpackage
|
||||
LOCALSYSTEM
|
||||
LOCATIONCHANGE
|
||||
LOCKMACHINE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
@@ -909,6 +928,7 @@ MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
measuretool
|
||||
meme
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
@@ -958,6 +978,7 @@ MOUSEHWHEEL
|
||||
MOUSEINPUT
|
||||
mousejump
|
||||
mousepointer
|
||||
mousepointercrosshairs
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
@@ -969,6 +990,7 @@ msc
|
||||
mscorlib
|
||||
msctls
|
||||
msdata
|
||||
msdia
|
||||
MSDL
|
||||
MSGFLT
|
||||
MSHCTX
|
||||
@@ -1157,6 +1179,18 @@ PARENTRELATIVEFORADDRESSBAR
|
||||
PARENTRELATIVEPARSING
|
||||
parray
|
||||
PARTIALCONFIRMATIONDIALOGTITLE
|
||||
pasteashtmlfile
|
||||
pasteashtmlfileshortcut
|
||||
pasteasjson
|
||||
pasteasjsonshortcut
|
||||
pasteasmarkdown
|
||||
pasteasmarkdownshortcut
|
||||
pasteasplaintext
|
||||
pasteasplaintextshortcut
|
||||
pasteaspngfile
|
||||
pasteaspngfileshortcut
|
||||
pasteastxtfile
|
||||
pasteastxtfileshortcut
|
||||
PATCOPY
|
||||
PATHMUSTEXIST
|
||||
PATINVERT
|
||||
@@ -1224,6 +1258,7 @@ Pomodoro
|
||||
Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
powerocr
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1364,6 +1399,7 @@ Removelnk
|
||||
renamable
|
||||
RENAMEONCOLLISION
|
||||
reparented
|
||||
reparenthotkey
|
||||
reparenting
|
||||
reportfileaccesses
|
||||
requery
|
||||
@@ -1613,6 +1649,7 @@ STYLECHANGED
|
||||
STYLECHANGING
|
||||
subkeys
|
||||
sublang
|
||||
Subdomain
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
@@ -1683,6 +1720,7 @@ THH
|
||||
THICKFRAME
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
thumbnailhotkey
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
timedate
|
||||
@@ -1697,6 +1735,7 @@ tlb
|
||||
tlbimp
|
||||
tlc
|
||||
TNP
|
||||
TOGGLEEASYMOUSE
|
||||
Toolhelp
|
||||
toolkitconverters
|
||||
toolwindow
|
||||
@@ -1710,6 +1749,7 @@ tracelogging
|
||||
tracerpt
|
||||
trackbar
|
||||
trafficmanager
|
||||
transcodetomp
|
||||
transicc
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
@@ -1824,6 +1864,7 @@ VSINSTALLDIR
|
||||
VSM
|
||||
vso
|
||||
vsonline
|
||||
VSpeed
|
||||
vstemplate
|
||||
vstest
|
||||
VSTHRD
|
||||
@@ -1960,10 +2001,13 @@ XNamespace
|
||||
Xoshiro
|
||||
XPels
|
||||
XPixel
|
||||
XPos
|
||||
XResource
|
||||
xsi
|
||||
XSpeed
|
||||
XStr
|
||||
xstyler
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
xxxxxx
|
||||
@@ -1973,7 +2017,10 @@ YIncrement
|
||||
yinle
|
||||
yinyue
|
||||
YPels
|
||||
YPos
|
||||
YResolution
|
||||
YSpeed
|
||||
YTimer
|
||||
YStr
|
||||
YVIRTUALSCREEN
|
||||
ZEROINIT
|
||||
|
||||
@@ -64,6 +64,10 @@ extends:
|
||||
tsa:
|
||||
enabled: true
|
||||
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:
|
||||
- stage: Build
|
||||
|
||||
@@ -788,6 +788,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.WebSearch.UnitTests\Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj", "{E816D7B2-4688-4ECB-97CC-3D8E798F3831}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -2850,6 +2858,38 @@ Global
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|x64.Build.0 = Debug|x64
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|x64.ActiveCfg = Release|x64
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|x64.Build.0 = Release|x64
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|x64.Build.0 = Debug|x64
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -3161,6 +3201,10 @@ Global
|
||||
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
@@ -71,6 +71,41 @@ When the user changes settings in the UI:
|
||||
3. The runner calls the `set_config` function on the appropriate module
|
||||
4. The module parses the JSON and applies the new settings
|
||||
|
||||
# Shortcut Conflict Detection
|
||||
|
||||
Steps to enable conflict detection for a hotkey:
|
||||
|
||||
### 1. Implement module interface for hotkeys
|
||||
Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional<HotkeyEx> GetHotkeyEx()`.
|
||||
|
||||
- If not yet implemented, you need to add it so that it returns all hotkeys used by the module.
|
||||
- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup.
|
||||
- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp`
|
||||
|
||||
### 2. Implement IHotkeyConfig in the module settings (UI side)
|
||||
Make sure the module’s settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`.
|
||||
|
||||
- This method should return all hotkeys used in the module.
|
||||
- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`).
|
||||
- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs`
|
||||
- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings.
|
||||
It provides both `getter` and `setter` methods to read and update the corresponding hotkey.
|
||||
Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey.
|
||||
This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`
|
||||
|
||||
### 3. Update the module’s ViewModel
|
||||
The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()`.
|
||||
|
||||
- This method should return all hotkeys, maintaining the same order as in steps 1 and 2.
|
||||
- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs`
|
||||
|
||||
### 4. Ensure the module’s Views call `OnPageLoaded()`
|
||||
Once the module’s view is loaded, make sure to invoke the ViewModel’s `OnPageLoaded()` method:
|
||||
```cs
|
||||
Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||
```
|
||||
- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
|
||||
|
||||
## Debugging Settings
|
||||
|
||||
To debug settings issues:
|
||||
|
||||
@@ -52,7 +52,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
|
||||
## Rules
|
||||
|
||||
- **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.
|
||||
- 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.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 18 KiB |
@@ -11,7 +11,7 @@
|
||||
<Fragment>
|
||||
<!-- Resource directories should be added only if the installer is built on the build farm -->
|
||||
<?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)">
|
||||
<!-- Resource file directories -->
|
||||
<?foreach Language in $(var.LocLanguageList)?>
|
||||
@@ -181,7 +181,7 @@
|
||||
</Component>
|
||||
<Component
|
||||
Id="ImageResizer_$(var.IdSafeLanguage)_Component"
|
||||
Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER"
|
||||
Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder"
|
||||
Guid="$(var.CompGUIDPrefix)02">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<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)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)WinUI3AppsInstallFolder" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" On="uninstall"/>
|
||||
<?undef IdSafeLanguage?>
|
||||
<?endforeach?>
|
||||
</Component>
|
||||
|
||||
@@ -112,7 +112,7 @@ private:
|
||||
return {};
|
||||
}
|
||||
|
||||
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
|
||||
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -122,6 +122,7 @@ private:
|
||||
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
||||
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
||||
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
||||
hotkey.isShown = isShown;
|
||||
return hotkey;
|
||||
}
|
||||
catch (...)
|
||||
@@ -231,8 +232,10 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue)
|
||||
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true)
|
||||
{
|
||||
bool actionIsShown = true;
|
||||
|
||||
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
|
||||
{
|
||||
return;
|
||||
@@ -240,9 +243,9 @@ private:
|
||||
|
||||
const auto action = actionValue.GetObjectW();
|
||||
|
||||
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
|
||||
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown)
|
||||
{
|
||||
return;
|
||||
actionIsShown = false;
|
||||
}
|
||||
|
||||
if (action.HasKey(JSON_KEY_SHORTCUT))
|
||||
@@ -250,7 +253,7 @@ private:
|
||||
const AdditionalAction additionalAction
|
||||
{
|
||||
actionName.c_str(),
|
||||
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
|
||||
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
|
||||
};
|
||||
|
||||
m_additional_actions.push_back(additionalAction);
|
||||
@@ -259,12 +262,12 @@ private:
|
||||
{
|
||||
for (const auto& [subActionName, subAction] : action)
|
||||
{
|
||||
process_additional_action(subActionName, subAction);
|
||||
process_additional_action(subActionName, subAction, actionIsShown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
const auto settingsObject = settings.get_raw_json();
|
||||
|
||||
@@ -317,9 +320,21 @@ private:
|
||||
{
|
||||
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
|
||||
|
||||
for (const auto& [actionName, additionalAction] : additionalActions)
|
||||
// Define the expected order to ensure consistent hotkey ID assignment
|
||||
const std::vector<winrt::hstring> expectedOrder = {
|
||||
L"image-to-text",
|
||||
L"paste-as-file",
|
||||
L"transcode"
|
||||
};
|
||||
|
||||
// Process actions in the predefined order
|
||||
for (auto& actionKey : expectedOrder)
|
||||
{
|
||||
process_additional_action(actionName, additionalAction);
|
||||
if (additionalActions.HasKey(actionKey))
|
||||
{
|
||||
const auto actionValue = additionalActions.GetNamedValue(actionKey);
|
||||
process_additional_action(actionKey, actionValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,17 +346,14 @@ private:
|
||||
for (const auto& customAction : customActions)
|
||||
{
|
||||
const auto object = customAction.GetObjectW();
|
||||
bool actionIsShown = object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false);
|
||||
|
||||
if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
|
||||
{
|
||||
const CustomAction customActionData
|
||||
{
|
||||
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
|
||||
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
|
||||
};
|
||||
const CustomAction customActionData{
|
||||
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
|
||||
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
|
||||
};
|
||||
|
||||
m_custom_actions.push_back(customActionData);
|
||||
}
|
||||
m_custom_actions.push_back(customActionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,5 +298,34 @@ namespace Hosts.Tests
|
||||
var hidden = fileSystem.FileInfo.New(service.HostsFilePath).Attributes.HasFlag(FileAttributes.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;
|
||||
|
||||
public bool NoLeadingSpaces { get; private set; }
|
||||
|
||||
public bool LoopbackDuplicates
|
||||
{
|
||||
get => _loopbackDuplicates;
|
||||
@@ -88,6 +90,7 @@ namespace Hosts.Settings
|
||||
AdditionalLinesPosition = (HostsAdditionalLinesPosition)settings.Properties.AdditionalLinesPosition;
|
||||
Encoding = (HostsEncoding)settings.Properties.Encoding;
|
||||
LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
|
||||
NoLeadingSpaces = settings.Properties.NoLeadingSpaces;
|
||||
}
|
||||
|
||||
retry = false;
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace HostsUILib.Helpers
|
||||
{
|
||||
lineBuilder.Append('#').Append(' ');
|
||||
}
|
||||
else if (anyDisabled)
|
||||
else if (anyDisabled && !_userSettings.NoLeadingSpaces)
|
||||
{
|
||||
lineBuilder.Append(' ').Append(' ');
|
||||
}
|
||||
|
||||
@@ -19,5 +19,7 @@ namespace HostsUILib.Settings
|
||||
event EventHandler LoopbackDuplicatesChanged;
|
||||
|
||||
public delegate void OpenSettingsFunction();
|
||||
|
||||
public bool NoLeadingSpaces { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,73 @@ struct InclusiveCrosshairs
|
||||
void SwitchActivationMode();
|
||||
void ApplySettings(InclusiveCrosshairsSettings& settings, bool applyToRuntimeObjects);
|
||||
|
||||
public:
|
||||
// Allow external callers to request a position update (thread-safe enqueue)
|
||||
static void RequestUpdatePosition()
|
||||
{
|
||||
if (instance != nullptr)
|
||||
{
|
||||
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
|
||||
dispatcherQueue.TryEnqueue([]() {
|
||||
if (instance != nullptr)
|
||||
{
|
||||
instance->UpdateCrosshairsPosition();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void EnsureOn()
|
||||
{
|
||||
if (instance != nullptr)
|
||||
{
|
||||
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
|
||||
dispatcherQueue.TryEnqueue([]() {
|
||||
if (instance != nullptr && !instance->m_drawing)
|
||||
{
|
||||
instance->StartDrawing();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void EnsureOff()
|
||||
{
|
||||
if (instance != nullptr)
|
||||
{
|
||||
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
|
||||
dispatcherQueue.TryEnqueue([]() {
|
||||
if (instance != nullptr && instance->m_drawing)
|
||||
{
|
||||
instance->StopDrawing();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void SetExternalControl(bool enabled)
|
||||
{
|
||||
if (instance != nullptr)
|
||||
{
|
||||
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
|
||||
dispatcherQueue.TryEnqueue([enabled]() {
|
||||
if (instance != nullptr)
|
||||
{
|
||||
instance->m_externalControl = enabled;
|
||||
if (enabled && instance->m_mouseHook)
|
||||
{
|
||||
UnhookWindowsHookEx(instance->m_mouseHook);
|
||||
instance->m_mouseHook = NULL;
|
||||
}
|
||||
else if (!enabled && instance->m_drawing && !instance->m_mouseHook)
|
||||
{
|
||||
instance->m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, instance->m_hinstance, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
enum class MouseButton
|
||||
{
|
||||
@@ -69,6 +136,7 @@ private:
|
||||
bool m_drawing = false;
|
||||
bool m_destroyed = false;
|
||||
bool m_hiddenCursor = false;
|
||||
bool m_externalControl = false;
|
||||
void SetAutoHideTimer() noexcept;
|
||||
|
||||
// Configurable Settings
|
||||
@@ -264,9 +332,12 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP
|
||||
if (nCode >= 0)
|
||||
{
|
||||
MSLLHOOKSTRUCT* hookData = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
|
||||
if (wParam == WM_MOUSEMOVE)
|
||||
if (instance && !instance->m_externalControl)
|
||||
{
|
||||
instance->UpdateCrosshairsPosition();
|
||||
if (wParam == WM_MOUSEMOVE)
|
||||
{
|
||||
instance->UpdateCrosshairsPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(0, nCode, wParam, lParam);
|
||||
@@ -527,6 +598,26 @@ bool InclusiveCrosshairsIsEnabled()
|
||||
return (InclusiveCrosshairs::instance != nullptr);
|
||||
}
|
||||
|
||||
void InclusiveCrosshairsRequestUpdatePosition()
|
||||
{
|
||||
InclusiveCrosshairs::RequestUpdatePosition();
|
||||
}
|
||||
|
||||
void InclusiveCrosshairsEnsureOn()
|
||||
{
|
||||
InclusiveCrosshairs::EnsureOn();
|
||||
}
|
||||
|
||||
void InclusiveCrosshairsEnsureOff()
|
||||
{
|
||||
InclusiveCrosshairs::EnsureOff();
|
||||
}
|
||||
|
||||
void InclusiveCrosshairsSetExternalControl(bool enabled)
|
||||
{
|
||||
InclusiveCrosshairs::SetExternalControl(enabled);
|
||||
}
|
||||
|
||||
int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings)
|
||||
{
|
||||
Logger::info("Starting a crosshairs instance.");
|
||||
|
||||
@@ -31,3 +31,7 @@ void InclusiveCrosshairsDisable();
|
||||
bool InclusiveCrosshairsIsEnabled();
|
||||
void InclusiveCrosshairsSwitch();
|
||||
void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings);
|
||||
void InclusiveCrosshairsRequestUpdatePosition();
|
||||
void InclusiveCrosshairsEnsureOn();
|
||||
void InclusiveCrosshairsEnsureOff();
|
||||
void InclusiveCrosshairsSetExternalControl(bool enabled);
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
#include "trace.h"
|
||||
#include "InclusiveCrosshairs.h"
|
||||
#include "common/utils/color.h"
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
extern void InclusiveCrosshairsRequestUpdatePosition();
|
||||
extern void InclusiveCrosshairsEnsureOn();
|
||||
extern void InclusiveCrosshairsEnsureOff();
|
||||
extern void InclusiveCrosshairsSetExternalControl(bool enabled);
|
||||
|
||||
// Non-Localizable strings
|
||||
namespace
|
||||
@@ -11,6 +20,7 @@ namespace
|
||||
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
|
||||
const wchar_t JSON_KEY_VALUE[] = L"value";
|
||||
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
|
||||
const wchar_t JSON_KEY_GLIDING_ACTIVATION_SHORTCUT[] = L"gliding_cursor_activation_shortcut";
|
||||
const wchar_t JSON_KEY_CROSSHAIRS_COLOR[] = L"crosshairs_color";
|
||||
const wchar_t JSON_KEY_CROSSHAIRS_OPACITY[] = L"crosshairs_opacity";
|
||||
const wchar_t JSON_KEY_CROSSHAIRS_RADIUS[] = L"crosshairs_radius";
|
||||
@@ -21,13 +31,15 @@ namespace
|
||||
const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled";
|
||||
const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length";
|
||||
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
|
||||
const wchar_t JSON_KEY_GLIDE_TRAVEL_SPEED[] = L"gliding_travel_speed";
|
||||
const wchar_t JSON_KEY_GLIDE_DELAY_SPEED[] = L"gliding_delay_speed";
|
||||
}
|
||||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
HMODULE m_hModule;
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
|
||||
{
|
||||
m_hModule = hModule;
|
||||
switch (ul_reason_for_call)
|
||||
@@ -57,8 +69,46 @@ private:
|
||||
// The PowerToy state.
|
||||
bool m_enabled = false;
|
||||
|
||||
// Hotkey to invoke the module
|
||||
HotkeyEx m_hotkey;
|
||||
// Additional hotkeys (legacy API) to support multiple shortcuts
|
||||
Hotkey m_activationHotkey{}; // Crosshairs toggle
|
||||
Hotkey m_glidingHotkey{}; // Gliding cursor state machine
|
||||
|
||||
// Shared state for worker threads (decoupled from this lifetime)
|
||||
struct State
|
||||
{
|
||||
std::atomic<bool> stopX{ false };
|
||||
std::atomic<bool> stopY{ false };
|
||||
|
||||
// positions and speeds
|
||||
int currentXPos{ 0 };
|
||||
int currentYPos{ 0 };
|
||||
int currentXSpeed{ 0 }; // pixels per base window
|
||||
int currentYSpeed{ 0 }; // pixels per base window
|
||||
int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan
|
||||
|
||||
// Fractional accumulators to spread movement across 10ms ticks
|
||||
double xFraction{ 0.0 };
|
||||
double yFraction{ 0.0 };
|
||||
|
||||
// Speeds represent pixels per 200ms (min 5, max 60 enforced by UI/settings)
|
||||
int fastHSpeed{ 30 }; // pixels per base window
|
||||
int slowHSpeed{ 5 }; // pixels per base window
|
||||
int fastVSpeed{ 30 }; // pixels per base window
|
||||
int slowVSpeed{ 5 }; // pixels per base window
|
||||
};
|
||||
|
||||
std::shared_ptr<State> m_state;
|
||||
|
||||
// Worker threads
|
||||
std::thread m_xThread;
|
||||
std::thread m_yThread;
|
||||
|
||||
// Gliding cursor state machine
|
||||
std::atomic<int> m_glideState{ 0 }; // 0..4 like the AHK script
|
||||
|
||||
// Timer configuration: 10ms tick, speeds are defined per 200ms base window
|
||||
static constexpr int kTimerTickMs = 10;
|
||||
static constexpr int kBaseSpeedTickMs = 200; // mapping period for configured pixel counts
|
||||
|
||||
// Mouse Pointer Crosshairs specific settings
|
||||
InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings;
|
||||
@@ -68,12 +118,17 @@ public:
|
||||
MousePointerCrosshairs()
|
||||
{
|
||||
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName);
|
||||
m_state = std::make_shared<State>();
|
||||
init_settings();
|
||||
};
|
||||
|
||||
// Destroy the powertoy and free memory
|
||||
virtual void destroy() override
|
||||
{
|
||||
StopXTimer();
|
||||
StopYTimer();
|
||||
// Release shared state so worker threads (if any) exit when weak_ptr lock fails
|
||||
m_state.reset();
|
||||
delete this;
|
||||
}
|
||||
|
||||
@@ -107,9 +162,7 @@ public:
|
||||
|
||||
// Signal from the Settings editor to call a custom action.
|
||||
// This can be used to spawn more complex editors.
|
||||
virtual void call_custom_action(const wchar_t* action) override
|
||||
{
|
||||
}
|
||||
virtual void call_custom_action(const wchar_t* /*action*/) override {}
|
||||
|
||||
// Called by the runner to pass the updated settings values as a serialized JSON.
|
||||
virtual void set_config(const wchar_t* config) override
|
||||
@@ -143,6 +196,9 @@ public:
|
||||
{
|
||||
m_enabled = false;
|
||||
Trace::EnableMousePointerCrosshairs(false);
|
||||
StopXTimer();
|
||||
StopYTimer();
|
||||
m_glideState = 0;
|
||||
InclusiveCrosshairsDisable();
|
||||
}
|
||||
|
||||
@@ -158,15 +214,249 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual std::optional<HotkeyEx> GetHotkeyEx() override
|
||||
// Legacy multi-hotkey support (like CropAndLock)
|
||||
virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override
|
||||
{
|
||||
return m_hotkey;
|
||||
if (buffer && buffer_size >= 2)
|
||||
{
|
||||
buffer[0] = m_activationHotkey; // Crosshairs toggle
|
||||
buffer[1] = m_glidingHotkey; // Gliding cursor toggle
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
virtual void OnHotkeyEx() override
|
||||
virtual bool on_hotkey(size_t hotkeyId) override
|
||||
{
|
||||
InclusiveCrosshairsSwitch();
|
||||
if (!m_enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hotkeyId == 0)
|
||||
{
|
||||
InclusiveCrosshairsSwitch();
|
||||
return true;
|
||||
}
|
||||
if (hotkeyId == 1)
|
||||
{
|
||||
HandleGlidingHotkey();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
static void LeftClick()
|
||||
{
|
||||
INPUT inputs[2]{};
|
||||
inputs[0].type = INPUT_MOUSE;
|
||||
inputs[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
||||
inputs[1].type = INPUT_MOUSE;
|
||||
inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
||||
SendInput(2, inputs, sizeof(INPUT));
|
||||
}
|
||||
|
||||
// Stateless helpers operating on shared State
|
||||
static void PositionCursorX(const std::shared_ptr<State>& s)
|
||||
{
|
||||
int screenW = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
s->currentYPos = screenH / 2;
|
||||
|
||||
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
|
||||
const double perTick = (static_cast<double>(s->currentXSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
|
||||
s->xFraction += perTick;
|
||||
int step = static_cast<int>(s->xFraction);
|
||||
if (step > 0)
|
||||
{
|
||||
s->xFraction -= step;
|
||||
s->currentXPos += step;
|
||||
}
|
||||
|
||||
s->xPosSnapshot = s->currentXPos;
|
||||
if (s->currentXPos >= screenW)
|
||||
{
|
||||
s->currentXPos = 0;
|
||||
s->currentXSpeed = s->fastHSpeed;
|
||||
s->xPosSnapshot = 0;
|
||||
s->xFraction = 0.0; // reset fractional remainder on wrap
|
||||
}
|
||||
SetCursorPos(s->currentXPos, s->currentYPos);
|
||||
// Ensure overlay crosshairs follow immediately
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
}
|
||||
|
||||
static void PositionCursorY(const std::shared_ptr<State>& s)
|
||||
{
|
||||
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
// Keep X at snapshot
|
||||
// Use s->xPosSnapshot captured during X pass
|
||||
|
||||
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
|
||||
const double perTick = (static_cast<double>(s->currentYSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
|
||||
s->yFraction += perTick;
|
||||
int step = static_cast<int>(s->yFraction);
|
||||
if (step > 0)
|
||||
{
|
||||
s->yFraction -= step;
|
||||
s->currentYPos += step;
|
||||
}
|
||||
|
||||
if (s->currentYPos >= screenH)
|
||||
{
|
||||
s->currentYPos = 0;
|
||||
s->currentYSpeed = s->fastVSpeed;
|
||||
s->yFraction = 0.0; // reset fractional remainder on wrap
|
||||
}
|
||||
SetCursorPos(s->xPosSnapshot, s->currentYPos);
|
||||
// Ensure overlay crosshairs follow immediately
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
}
|
||||
|
||||
void StartXTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s->stopX = false;
|
||||
std::weak_ptr<State> wp = s;
|
||||
m_xThread = std::thread([wp]() {
|
||||
while (true)
|
||||
{
|
||||
auto sp = wp.lock();
|
||||
if (!sp || sp->stopX.load())
|
||||
{
|
||||
break;
|
||||
}
|
||||
PositionCursorX(sp);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void StopXTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (s)
|
||||
{
|
||||
s->stopX = true;
|
||||
}
|
||||
if (m_xThread.joinable())
|
||||
{
|
||||
m_xThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void StartYTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s->stopY = false;
|
||||
std::weak_ptr<State> wp = s;
|
||||
m_yThread = std::thread([wp]() {
|
||||
while (true)
|
||||
{
|
||||
auto sp = wp.lock();
|
||||
if (!sp || sp->stopY.load())
|
||||
{
|
||||
break;
|
||||
}
|
||||
PositionCursorY(sp);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void StopYTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (s)
|
||||
{
|
||||
s->stopY = true;
|
||||
}
|
||||
if (m_yThread.joinable())
|
||||
{
|
||||
m_yThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void HandleGlidingHotkey()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Simulate the AHK state machine
|
||||
int state = m_glideState.load();
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
// Ensure crosshairs on (do not toggle off if already on)
|
||||
InclusiveCrosshairsEnsureOn();
|
||||
// Disable internal mouse hook so we control position updates explicitly
|
||||
InclusiveCrosshairsSetExternalControl(true);
|
||||
|
||||
s->currentXPos = 0;
|
||||
s->currentXSpeed = s->fastHSpeed;
|
||||
s->xFraction = 0.0;
|
||||
s->yFraction = 0.0;
|
||||
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
|
||||
SetCursorPos(0, y);
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
m_glideState = 1;
|
||||
StartXTimer();
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
// Slow horizontal
|
||||
s->currentXSpeed = s->slowHSpeed;
|
||||
m_glideState = 2;
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
// Stop horizontal, start vertical (fast)
|
||||
StopXTimer();
|
||||
s->currentYSpeed = s->fastVSpeed;
|
||||
s->currentYPos = 0;
|
||||
s->yFraction = 0.0;
|
||||
SetCursorPos(s->xPosSnapshot, s->currentYPos);
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
m_glideState = 3;
|
||||
StartYTimer();
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
// Slow vertical
|
||||
s->currentYSpeed = s->slowVSpeed;
|
||||
m_glideState = 4;
|
||||
break;
|
||||
}
|
||||
case 4:
|
||||
default:
|
||||
{
|
||||
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
|
||||
StopYTimer();
|
||||
m_glideState = 0;
|
||||
LeftClick();
|
||||
InclusiveCrosshairsEnsureOff();
|
||||
InclusiveCrosshairsSetExternalControl(false);
|
||||
s->xFraction = 0.0;
|
||||
s->yFraction = 0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the settings file.
|
||||
void init_settings()
|
||||
{
|
||||
@@ -192,37 +482,44 @@ public:
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse HotKey
|
||||
// Parse primary activation HotKey (for centralized hook)
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
|
||||
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
|
||||
m_hotkey = HotkeyEx();
|
||||
if (hotkey.win_pressed())
|
||||
{
|
||||
m_hotkey.modifiersMask |= MOD_WIN;
|
||||
}
|
||||
|
||||
if (hotkey.ctrl_pressed())
|
||||
{
|
||||
m_hotkey.modifiersMask |= MOD_CONTROL;
|
||||
}
|
||||
|
||||
if (hotkey.shift_pressed())
|
||||
{
|
||||
m_hotkey.modifiersMask |= MOD_SHIFT;
|
||||
}
|
||||
|
||||
if (hotkey.alt_pressed())
|
||||
{
|
||||
m_hotkey.modifiersMask |= MOD_ALT;
|
||||
}
|
||||
|
||||
m_hotkey.vkCode = hotkey.get_code();
|
||||
// Map to legacy Hotkey for multi-hotkey API
|
||||
m_activationHotkey.win = hotkey.win_pressed();
|
||||
m_activationHotkey.ctrl = hotkey.ctrl_pressed();
|
||||
m_activationHotkey.shift = hotkey.shift_pressed();
|
||||
m_activationHotkey.alt = hotkey.alt_pressed();
|
||||
m_activationHotkey.key = static_cast<unsigned char>(hotkey.get_code());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse Gliding Cursor HotKey
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT);
|
||||
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
|
||||
m_glidingHotkey.win = hotkey.win_pressed();
|
||||
m_glidingHotkey.ctrl = hotkey.ctrl_pressed();
|
||||
m_glidingHotkey.shift = hotkey.shift_pressed();
|
||||
m_glidingHotkey.alt = hotkey.alt_pressed();
|
||||
m_glidingHotkey.key = static_cast<unsigned char>(hotkey.get_code());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// note that this is also defined in src\settings-ui\Settings.UI.Library\MousePointerCrosshairsProperties.cs, DefaultGlidingCursorActivationShortcut
|
||||
// both need to be kept in sync!
|
||||
Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+.");
|
||||
m_glidingHotkey.win = true;
|
||||
m_glidingHotkey.alt = true;
|
||||
m_glidingHotkey.ctrl = false;
|
||||
m_glidingHotkey.shift = false;
|
||||
m_glidingHotkey.key = VK_OEM_PERIOD;
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse Opacity
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY);
|
||||
@@ -272,7 +569,6 @@ public:
|
||||
{
|
||||
throw std::runtime_error("Invalid Radius value");
|
||||
}
|
||||
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -291,7 +587,6 @@ public:
|
||||
{
|
||||
throw std::runtime_error("Invalid Thickness value");
|
||||
}
|
||||
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -320,7 +615,7 @@ public:
|
||||
{
|
||||
// Parse border size
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE);
|
||||
int value = static_cast <int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 0)
|
||||
{
|
||||
inclusiveCrosshairsSettings.crosshairsBorderSize = value;
|
||||
@@ -383,20 +678,86 @@ public:
|
||||
{
|
||||
Logger::warn("Failed to initialize auto activate from settings. Will use default value");
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse Travel speed (fast speed mapping)
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_TRAVEL_SPEED);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 5 && value <= 60)
|
||||
{
|
||||
m_state->fastHSpeed = value;
|
||||
m_state->fastVSpeed = value;
|
||||
}
|
||||
else if (value < 5)
|
||||
{
|
||||
m_state->fastHSpeed = 5; m_state->fastVSpeed = 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_state->fastHSpeed = 60; m_state->fastVSpeed = 60;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize gliding travel speed from settings. Using default 25.");
|
||||
if (m_state)
|
||||
{
|
||||
m_state->fastHSpeed = 25;
|
||||
m_state->fastVSpeed = 25;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
// Parse Delay speed (slow speed mapping)
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_DELAY_SPEED);
|
||||
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
if (value >= 5 && value <= 60)
|
||||
{
|
||||
m_state->slowHSpeed = value;
|
||||
m_state->slowVSpeed = value;
|
||||
}
|
||||
else if (value < 5)
|
||||
{
|
||||
m_state->slowHSpeed = 5; m_state->slowVSpeed = 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_state->slowHSpeed = 60; m_state->slowVSpeed = 60;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize gliding delay speed from settings. Using default 5.");
|
||||
if (m_state)
|
||||
{
|
||||
m_state->slowHSpeed = 5;
|
||||
m_state->slowVSpeed = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info("Mouse Pointer Crosshairs settings are empty");
|
||||
}
|
||||
if (!m_hotkey.modifiersMask)
|
||||
|
||||
if (m_activationHotkey.key == 0)
|
||||
{
|
||||
Logger::info("Mouse Pointer Crosshairs is going to use default shortcut");
|
||||
m_hotkey.modifiersMask = MOD_WIN | MOD_ALT;
|
||||
m_hotkey.vkCode = 0x50; // P key
|
||||
m_activationHotkey.win = true;
|
||||
m_activationHotkey.alt = true;
|
||||
m_activationHotkey.ctrl = false;
|
||||
m_activationHotkey.shift = false;
|
||||
m_activationHotkey.key = 'P';
|
||||
}
|
||||
if (m_glidingHotkey.key == 0)
|
||||
{
|
||||
m_glidingHotkey.win = true;
|
||||
m_glidingHotkey.alt = true;
|
||||
m_glidingHotkey.ctrl = false;
|
||||
m_glidingHotkey.shift = false;
|
||||
m_glidingHotkey.key = VK_OEM_PERIOD;
|
||||
}
|
||||
m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
|
||||
@@ -556,6 +556,61 @@ public:
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
||||
{
|
||||
constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
|
||||
|
||||
if (hotkeys && buffer_size >= num_hotkeys)
|
||||
{
|
||||
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
|
||||
|
||||
// Cache the raw JSON object to avoid multiple parsing
|
||||
json::JsonObject root_json = values.get_raw_json();
|
||||
json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
|
||||
|
||||
size_t hotkey_index = 0;
|
||||
|
||||
// Helper lambda to extract hotkey from JSON properties
|
||||
auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey {
|
||||
if (properties_json.HasKey(property_name))
|
||||
{
|
||||
try
|
||||
{
|
||||
json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
|
||||
|
||||
// Extract hotkey properties directly from JSON
|
||||
bool win = hotkey_json.GetNamedBoolean(L"win", false);
|
||||
bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false);
|
||||
bool alt = hotkey_json.GetNamedBoolean(L"alt", false);
|
||||
bool shift = hotkey_json.GetNamedBoolean(L"shift", false);
|
||||
unsigned char key = static_cast<unsigned char>(
|
||||
hotkey_json.GetNamedNumber(L"code", 0));
|
||||
|
||||
return { win, ctrl, shift, alt, key };
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// If parsing individual hotkey fails, use defaults
|
||||
return { false, false, false, false, 0 };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Property doesn't exist, use defaults
|
||||
return { false, false, false, false, 0 };
|
||||
}
|
||||
};
|
||||
|
||||
// Extract all hotkeys using the optimized helper
|
||||
hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse
|
||||
hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine
|
||||
hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs
|
||||
hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect
|
||||
}
|
||||
|
||||
return num_hotkeys;
|
||||
}
|
||||
|
||||
void launch_add_firewall_process()
|
||||
{
|
||||
Logger::trace(L"Starting Process to add firewall rule");
|
||||
|
||||
@@ -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>
|
||||
public void LogMessage(ILogMessage message)
|
||||
{
|
||||
if (Host != null)
|
||||
if (Host is not null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
@@ -47,7 +47,7 @@ public partial class ExtensionHostInstance
|
||||
|
||||
public void ShowStatus(IStatusMessage message, StatusContext context)
|
||||
{
|
||||
if (Host != null)
|
||||
if (Host is not null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
@@ -64,7 +64,7 @@ public partial class ExtensionHostInstance
|
||||
|
||||
public void HideStatus(IStatusMessage message)
|
||||
{
|
||||
if (Host != null)
|
||||
if (Host is not null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
||||
|
||||
public IAsyncAction HideStatus(IStatusMessage? message)
|
||||
{
|
||||
if (message == null)
|
||||
if (message is null)
|
||||
{
|
||||
return Task.CompletedTask.AsAsyncAction();
|
||||
}
|
||||
@@ -55,7 +55,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
||||
|
||||
public IAsyncAction LogMessage(ILogMessage? message)
|
||||
{
|
||||
if (message == null)
|
||||
if (message is null)
|
||||
{
|
||||
return Task.CompletedTask.AsAsyncAction();
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
||||
try
|
||||
{
|
||||
var vm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
|
||||
if (vm != null)
|
||||
if (vm is not null)
|
||||
{
|
||||
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
|
||||
var oldVm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault();
|
||||
if (oldVm != null)
|
||||
if (oldVm is not null)
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
() =>
|
||||
@@ -142,7 +142,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
||||
|
||||
public IAsyncAction ShowStatus(IStatusMessage? message, StatusContext context)
|
||||
{
|
||||
if (message == null)
|
||||
if (message is null)
|
||||
{
|
||||
return Task.CompletedTask.AsAsyncAction();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
@@ -35,13 +34,13 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
[NotifyPropertyChangedFor(nameof(HasPrimaryCommand))]
|
||||
public partial CommandItemViewModel? PrimaryCommand { get; set; }
|
||||
|
||||
public bool HasPrimaryCommand => PrimaryCommand != null && PrimaryCommand.ShouldBeVisible;
|
||||
public bool HasPrimaryCommand => PrimaryCommand is not null && PrimaryCommand.ShouldBeVisible;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasSecondaryCommand))]
|
||||
public partial CommandItemViewModel? SecondaryCommand { get; set; }
|
||||
|
||||
public bool HasSecondaryCommand => SecondaryCommand != null;
|
||||
public bool HasSecondaryCommand => SecondaryCommand is not null;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShouldShowContextMenu { get; set; } = false;
|
||||
@@ -58,14 +57,14 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
private void SetSelectedItem(ICommandBarContext? value)
|
||||
{
|
||||
if (value != null)
|
||||
if (value is not null)
|
||||
{
|
||||
PrimaryCommand = value.PrimaryCommand;
|
||||
value.PropertyChanged += SelectedItemPropertyChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SelectedItem != null)
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
SelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
|
||||
}
|
||||
@@ -88,7 +87,7 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
private void UpdateContextItems()
|
||||
{
|
||||
if (SelectedItem == null)
|
||||
if (SelectedItem is null)
|
||||
{
|
||||
SecondaryCommand = null;
|
||||
ShouldShowContextMenu = false;
|
||||
@@ -127,13 +126,13 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||
{
|
||||
var keybindings = SelectedItem?.Keybindings();
|
||||
if (keybindings != null)
|
||||
if (keybindings is not null)
|
||||
{
|
||||
// Does the pressed key match any of the keybindings?
|
||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
||||
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)
|
||||
{
|
||||
if (command == null)
|
||||
if (command is null)
|
||||
{
|
||||
return ContextKeybindingResult.Unhandled;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
|
||||
|
||||
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()
|
||||
{
|
||||
@@ -32,7 +32,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
|
||||
base.InitializeProperties();
|
||||
|
||||
var contextItem = Model.Unsafe;
|
||||
if (contextItem == null)
|
||||
if (contextItem is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
{
|
||||
get
|
||||
{
|
||||
List<IContextItemViewModel> l = _defaultCommandContextItem == null ?
|
||||
List<IContextItemViewModel> l = _defaultCommandContextItem is null ?
|
||||
new() :
|
||||
[_defaultCommandContextItem];
|
||||
|
||||
@@ -100,7 +100,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
|
||||
var model = _commandItemModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
|
||||
var model = _commandItemModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +136,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
Command.InitializeProperties();
|
||||
|
||||
var listIcon = model.Icon;
|
||||
if (listIcon != null)
|
||||
if (listIcon is not null)
|
||||
{
|
||||
_listItemIcon = new(listIcon);
|
||||
_listItemIcon.InitializeProperties();
|
||||
@@ -172,13 +172,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
|
||||
var model = _commandItemModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var more = model.MoreCommands;
|
||||
if (more != null)
|
||||
if (more is not null)
|
||||
{
|
||||
MoreCommands = more
|
||||
.Select(item =>
|
||||
@@ -300,7 +300,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._commandItemModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -308,7 +308,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Command):
|
||||
if (Command != null)
|
||||
if (Command is not null)
|
||||
{
|
||||
Command.PropertyChanged -= Command_PropertyChanged;
|
||||
}
|
||||
@@ -339,7 +339,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
case nameof(model.MoreCommands):
|
||||
var more = model.MoreCommands;
|
||||
if (more != null)
|
||||
if (more is not null)
|
||||
{
|
||||
var newContextMenu = more
|
||||
.Select(item =>
|
||||
@@ -394,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
|
||||
// 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;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
_itemTitle = model.Title;
|
||||
}
|
||||
@@ -430,7 +430,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
Command.SafeCleanup();
|
||||
|
||||
var model = _commandItemModel.Unsafe;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
}
|
||||
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -67,13 +67,13 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
}
|
||||
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ico = model.Icon;
|
||||
if (ico != null)
|
||||
if (ico is not null)
|
||||
{
|
||||
Icon = new(ico);
|
||||
Icon.InitializeProperties();
|
||||
@@ -98,7 +98,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
protected void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -125,7 +125,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
Icon = new(null); // necessary?
|
||||
|
||||
var model = Model.Unsafe;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public partial class ConfirmResultViewModel(IConfirmationArgs _args, WeakReferen
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
public DetailsViewModel? Details { get; private set; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Details))]
|
||||
public bool HasDetails => Details != null;
|
||||
public bool HasDetails => Details is not null;
|
||||
|
||||
/////// ICommandBarContext ///////
|
||||
public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
|
||||
@@ -67,7 +67,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
var viewModel = ViewModelFromContent(item, PageContext);
|
||||
if (viewModel != null)
|
||||
if (viewModel is not null)
|
||||
{
|
||||
viewModel.InitializeProperties();
|
||||
newContent.Add(viewModel);
|
||||
@@ -104,7 +104,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
base.InitializeProperties();
|
||||
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
});
|
||||
|
||||
var extensionDetails = model.Details;
|
||||
if (extensionDetails != null)
|
||||
if (extensionDetails is not null)
|
||||
{
|
||||
Details = new(extensionDetails, PageContext);
|
||||
Details.InitializeProperties();
|
||||
@@ -156,7 +156,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
base.FetchProperty(propertyName);
|
||||
|
||||
var model = this._model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -166,7 +166,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
case nameof(Commands):
|
||||
|
||||
var more = model.Commands;
|
||||
if (more != null)
|
||||
if (more is not null)
|
||||
{
|
||||
var newContextMenu = more
|
||||
.ToList()
|
||||
@@ -216,7 +216,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
break;
|
||||
case nameof(Details):
|
||||
var extensionDetails = model.Details;
|
||||
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
|
||||
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||
UpdateDetails();
|
||||
break;
|
||||
}
|
||||
@@ -248,7 +248,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
[RelayCommand]
|
||||
private void InvokePrimaryCommand(ContentPageViewModel page)
|
||||
{
|
||||
if (PrimaryCommand != null)
|
||||
if (PrimaryCommand is not null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
|
||||
}
|
||||
@@ -258,7 +258,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
[RelayCommand]
|
||||
private void InvokeSecondaryCommand(ContentPageViewModel page)
|
||||
{
|
||||
if (SecondaryCommand != null)
|
||||
if (SecondaryCommand is not null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
|
||||
}
|
||||
@@ -285,7 +285,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
Content.Clear();
|
||||
|
||||
var model = _model.Unsafe;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
model.ItemsChanged -= Model_ItemsChanged;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Diagnostics.Utilities;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
@@ -51,7 +50,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
|
||||
public void UpdateContextItems()
|
||||
{
|
||||
if (SelectedItem != null)
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
if (SelectedItem.MoreCommands.Count() > 1)
|
||||
{
|
||||
@@ -68,14 +67,14 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedItem == null)
|
||||
if (SelectedItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSearchText = searchText;
|
||||
|
||||
if (CurrentContextMenu == null)
|
||||
if (CurrentContextMenu is null)
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, []);
|
||||
return;
|
||||
@@ -124,7 +123,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
/// that have a shortcut key set.</returns>
|
||||
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
||||
{
|
||||
if (CurrentContextMenu == null)
|
||||
if (CurrentContextMenu is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
@@ -140,7 +139,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||
{
|
||||
var keybindings = Keybindings();
|
||||
if (keybindings != null)
|
||||
if (keybindings is not null)
|
||||
{
|
||||
// Does the pressed key match any of the keybindings?
|
||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
||||
@@ -190,7 +189,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
OnPropertyChanging(nameof(CurrentContextMenu));
|
||||
OnPropertyChanged(nameof(CurrentContextMenu));
|
||||
|
||||
if (CurrentContextMenu != null)
|
||||
if (CurrentContextMenu is not null)
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]);
|
||||
}
|
||||
@@ -198,7 +197,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
|
||||
public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command)
|
||||
{
|
||||
if (command == null)
|
||||
if (command is null)
|
||||
{
|
||||
return ContextKeybindingResult.Unhandled;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public partial class DetailsCommandsViewModel(
|
||||
{
|
||||
base.InitializeProperties();
|
||||
var model = _dataModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public abstract partial class DetailsElementViewModel(IDetailsElement _detailsEl
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public partial class DetailsLinkViewModel(
|
||||
|
||||
public Uri? Link { get; private set; }
|
||||
|
||||
public bool IsLink => Link != null;
|
||||
public bool IsLink => Link is not null;
|
||||
|
||||
public bool IsText => !IsLink;
|
||||
|
||||
@@ -26,14 +26,14 @@ public partial class DetailsLinkViewModel(
|
||||
{
|
||||
base.InitializeProperties();
|
||||
var model = _dataModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Text = model.Text ?? string.Empty;
|
||||
Link = model.Link;
|
||||
if (string.IsNullOrEmpty(Text) && Link != null)
|
||||
if (string.IsNullOrEmpty(Text) && Link is not null)
|
||||
{
|
||||
Text = Link.ToString();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public partial class DetailsTagsViewModel(
|
||||
{
|
||||
base.InitializeProperties();
|
||||
var model = _dataModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _detailsModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
UpdateProperty(nameof(HeroImage));
|
||||
|
||||
var meta = model.Metadata;
|
||||
if (meta != null)
|
||||
if (meta is not null)
|
||||
{
|
||||
foreach (var element in meta)
|
||||
{
|
||||
@@ -53,7 +53,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference<IPageCont
|
||||
IDetailsTags => new DetailsTagsViewModel(element, this.PageContext),
|
||||
_ => null,
|
||||
};
|
||||
if (vm != null)
|
||||
if (vm is not null)
|
||||
{
|
||||
vm.InitializeProperties();
|
||||
Metadata.Add(vm);
|
||||
|
||||
@@ -16,7 +16,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
// 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
|
||||
// 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.
|
||||
public string Icon { get; private set; } = string.Empty;
|
||||
@@ -36,7 +36,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
public void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo
|
||||
|
||||
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;
|
||||
|
||||
@@ -43,7 +43,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo
|
||||
public void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
public DetailsViewModel? Details { get; private set; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Details))]
|
||||
public bool HasDetails => Details != null;
|
||||
public bool HasDetails => Details is not null;
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
@@ -40,7 +40,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
base.InitializeProperties();
|
||||
|
||||
var li = Model.Unsafe;
|
||||
if (li == null)
|
||||
if (li is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
TextToSuggest = li.TextToSuggest;
|
||||
Section = li.Section ?? string.Empty;
|
||||
var extensionDetails = li.Details;
|
||||
if (extensionDetails != null)
|
||||
if (extensionDetails is not null)
|
||||
{
|
||||
Details = new(extensionDetails, PageContext);
|
||||
Details.InitializeProperties();
|
||||
@@ -67,7 +67,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
base.FetchProperty(propertyName);
|
||||
|
||||
var model = this.Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -85,7 +85,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
break;
|
||||
case nameof(Details):
|
||||
var extensionDetails = model.Details;
|
||||
Details = extensionDetails != null ? new(extensionDetails, PageContext) : null;
|
||||
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
|
||||
Details?.InitializeProperties();
|
||||
UpdateProperty(nameof(Details));
|
||||
UpdateProperty(nameof(HasDetails));
|
||||
@@ -136,7 +136,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
|
||||
Details?.SafeCleanup();
|
||||
|
||||
var model = Model.Unsafe;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
// We don't need to revoke the PropChanged event handler here,
|
||||
// because we are just overriding CommandItem's FetchProperty and
|
||||
|
||||
@@ -63,6 +63,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
private Task? _initializeItemsTask;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private CancellationTokenSource? _fetchItemsCancellationTokenSource;
|
||||
|
||||
private ListItemViewModel? _lastSelectedItem;
|
||||
|
||||
@@ -129,22 +130,38 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
||||
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
|
||||
// see 9806fe5d8 for the last commit that had this with sections
|
||||
_isFetching = true;
|
||||
|
||||
// Collect all the items into new viewmodels
|
||||
Collection<ListItemViewModel> newViewModels = [];
|
||||
|
||||
try
|
||||
{
|
||||
// Check for cancellation before starting expensive operations
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var newItems = _model.Unsafe!.GetItems();
|
||||
|
||||
// Collect all the items into new viewmodels
|
||||
Collection<ListItemViewModel> newViewModels = [];
|
||||
// Check for cancellation after getting items from extension
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// TODO we can probably further optimize this by also keeping a
|
||||
// HashSet of every ExtensionObject we currently have, and only
|
||||
// building new viewmodels for the ones we haven't already built.
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
// Check for cancellation during item processing
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
ListItemViewModel viewModel = new(item, new(this));
|
||||
|
||||
// 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);
|
||||
foreach (var item in firstTwenty)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
item?.SafeInitializeProperties();
|
||||
}
|
||||
|
||||
// Cancel any ongoing search
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
||||
// Check for cancellation before updating the list
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
List<ListItemViewModel> removedItems = [];
|
||||
lock (_listLock)
|
||||
{
|
||||
// Now that we have new ViewModels for everything from the
|
||||
// extension, smartly update our list of VMs
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels);
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems);
|
||||
|
||||
// DO NOT ThrowIfCancellationRequested AFTER THIS! If you do,
|
||||
// you'll clean up list items that we've now transferred into
|
||||
// .Items
|
||||
}
|
||||
|
||||
// If we removed items, we need to clean them up, to remove our event handlers
|
||||
foreach (var removedItem in removedItems)
|
||||
{
|
||||
removedItem.SafeCleanup();
|
||||
}
|
||||
|
||||
// TODO: Iterate over everything in Items, and prune items from the
|
||||
// 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)
|
||||
{
|
||||
// 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]
|
||||
private void InvokeItem(ListItemViewModel? item)
|
||||
{
|
||||
if (item != null)
|
||||
if (item is not null)
|
||||
{
|
||||
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(
|
||||
EmptyContent.PrimaryCommand.Command.Model,
|
||||
@@ -314,14 +363,14 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
[RelayCommand]
|
||||
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));
|
||||
}
|
||||
}
|
||||
else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null)
|
||||
else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe is not null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(
|
||||
EmptyContent.SecondaryCommand.Command.Model,
|
||||
@@ -332,12 +381,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
[RelayCommand]
|
||||
private void UpdateSelectedItem(ListItemViewModel? item)
|
||||
{
|
||||
if (_lastSelectedItem != null)
|
||||
if (_lastSelectedItem is not null)
|
||||
{
|
||||
_lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
if (item is not null)
|
||||
{
|
||||
SetSelectedItem(item);
|
||||
}
|
||||
@@ -383,7 +432,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var item = _lastSelectedItem;
|
||||
if (item == null)
|
||||
if (item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -438,7 +487,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
base.InitializeProperties();
|
||||
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -465,7 +514,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
public void LoadMoreIfNeeded()
|
||||
{
|
||||
var model = this._model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -509,7 +558,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
base.FetchProperty(propertyName);
|
||||
|
||||
var model = this._model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -540,7 +589,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
private void UpdateEmptyContent()
|
||||
{
|
||||
UpdateProperty(nameof(ShowEmptyContent));
|
||||
if (!ShowEmptyContent || EmptyContent.Model.Unsafe == null)
|
||||
if (!ShowEmptyContent || EmptyContent.Model.Unsafe is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -560,6 +609,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
|
||||
_fetchItemsCancellationTokenSource?.Cancel();
|
||||
_fetchItemsCancellationTokenSource?.Dispose();
|
||||
_fetchItemsCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
@@ -570,6 +623,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
EmptyContent = new(new(null), PageContext); // necessary?
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_fetchItemsCancellationTokenSource?.Cancel();
|
||||
|
||||
lock (_listLock)
|
||||
{
|
||||
@@ -588,7 +642,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
}
|
||||
|
||||
var model = _model.Unsafe;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
model.ItemsChanged -= Model_ItemsChanged;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public partial class LogMessageViewModel : ExtensionObjectViewModel
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
@@ -45,7 +46,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
[ObservableProperty]
|
||||
public partial AppExtensionHost ExtensionHost { get; private set; }
|
||||
|
||||
public bool HasStatusMessage => MostRecentStatusMessage != null;
|
||||
public bool HasStatusMessage => MostRecentStatusMessage is not null;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasStatusMessage))]
|
||||
@@ -132,7 +133,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var page = _pageModel.Unsafe;
|
||||
if (page == null)
|
||||
if (page is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -177,7 +178,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._pageModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -223,9 +224,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
extensionHint ??= ExtensionHost.GetExtensionDisplayName() ?? Title;
|
||||
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,
|
||||
TaskCreationOptions.None,
|
||||
Scheduler);
|
||||
@@ -240,7 +242,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
ExtensionHost.StatusMessages.CollectionChanged -= StatusMessages_CollectionChanged;
|
||||
|
||||
var model = _pageModel.Unsafe;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this.Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
////LoadedState = ViewModelLoadedState.Loading;
|
||||
if (!viewModel.IsInitialized
|
||||
&& viewModel.InitializeCommand != null)
|
||||
&& viewModel.InitializeCommand is not null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
@@ -185,7 +185,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
private void PerformCommand(PerformCommandMessage message)
|
||||
{
|
||||
var command = message.Command.Unsafe;
|
||||
if (command == null)
|
||||
if (command is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -205,7 +205,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||
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}");
|
||||
throw new NotSupportedException();
|
||||
@@ -240,7 +240,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
// TODO GH #525 This needs more better locking.
|
||||
lock (_invokeLock)
|
||||
{
|
||||
if (_handleInvokeTask != null)
|
||||
if (_handleInvokeTask is not null)
|
||||
{
|
||||
// do nothing - a command is already doing a thing
|
||||
}
|
||||
@@ -280,7 +280,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
private void UnsafeHandleCommandResult(ICommandResult? result)
|
||||
{
|
||||
if (result == null)
|
||||
if (result is null)
|
||||
{
|
||||
// No result, nothing to do.
|
||||
return;
|
||||
|
||||
@@ -17,7 +17,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
||||
|
||||
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)
|
||||
: base(context)
|
||||
@@ -28,7 +28,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
||||
Message = model.Message;
|
||||
State = model.State;
|
||||
var modelProgress = model.Progress;
|
||||
if (modelProgress != null)
|
||||
if (modelProgress is not null)
|
||||
{
|
||||
Progress = new(modelProgress, this.PageContext);
|
||||
Progress.InitializeProperties();
|
||||
@@ -61,7 +61,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this.Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -76,7 +76,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel
|
||||
break;
|
||||
case nameof(Progress):
|
||||
var modelProgress = model.Progress;
|
||||
if (modelProgress != null)
|
||||
if (modelProgress is not null)
|
||||
{
|
||||
Progress = new(modelProgress, this.PageContext);
|
||||
Progress.InitializeProperties();
|
||||
|
||||
@@ -28,7 +28,7 @@ public partial class TagViewModel(ITag _tag, WeakReference<IPageContext> context
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _tagModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public partial class AliasManager : ObservableObject
|
||||
try
|
||||
{
|
||||
var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
|
||||
if (topLevelCommand != null)
|
||||
if (topLevelCommand is not null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
|
||||
|
||||
@@ -88,7 +88,7 @@ public partial class AliasManager : ObservableObject
|
||||
}
|
||||
|
||||
// If we already have _this exact alias_, do nothing
|
||||
if (newAlias != null &&
|
||||
if (newAlias is not null &&
|
||||
_aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
||||
{
|
||||
if (existingAlias.CommandId == commandId)
|
||||
@@ -113,7 +113,7 @@ public partial class AliasManager : ObservableObject
|
||||
_aliases.Remove(alias.SearchPrefix);
|
||||
}
|
||||
|
||||
if (newAlias != null)
|
||||
if (newAlias is not null)
|
||||
{
|
||||
AddAlias(newAlias);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public partial class AppStateModel : ObservableObject
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed class CommandProviderWrapper
|
||||
{
|
||||
public bool IsExtension => Extension != null;
|
||||
public bool IsExtension => Extension is not null;
|
||||
|
||||
private readonly bool isValid;
|
||||
|
||||
@@ -188,14 +188,14 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
return topLevelViewModel;
|
||||
};
|
||||
if (commands != null)
|
||||
if (commands is not null)
|
||||
{
|
||||
TopLevelItems = commands
|
||||
.Select(c => makeAndAdd(c, false))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (fallbacks != null)
|
||||
if (fallbacks is not null)
|
||||
{
|
||||
FallbackItems = fallbacks
|
||||
.Select(c => makeAndAdd(c, true))
|
||||
|
||||
@@ -18,18 +18,18 @@ public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings,
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
public bool HasSettings =>
|
||||
_model.Unsafe != null && // We have a settings model AND
|
||||
(!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page
|
||||
_model.Unsafe is not null && // We have a settings model AND
|
||||
(!Initialized || SettingsPage is not null); // we weren't initialized, OR we were, and we do have a settings page
|
||||
|
||||
private void UnsafeInitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.SettingsPage != null)
|
||||
if (model.SettingsPage is not null)
|
||||
{
|
||||
SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost);
|
||||
SettingsPage.InitializeProperties();
|
||||
|
||||
@@ -30,7 +30,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
|
||||
public override ICommandResult SubmitForm(string inputs, string data)
|
||||
{
|
||||
var dataInput = JsonNode.Parse(data)?.AsObject();
|
||||
if (dataInput == null)
|
||||
if (dataInput is null)
|
||||
{
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public partial class LogMessagesPage : ListPage
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -203,7 +203,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
// If we don't have any previous filter results to work with, start
|
||||
// with a list of all our commands & apps.
|
||||
if (_filteredItems == null)
|
||||
if (_filteredItems is null)
|
||||
{
|
||||
_filteredItems = commands;
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
|
||||
@@ -98,7 +98,7 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var formInput = JsonNode.Parse(payload)?.AsObject();
|
||||
if (formInput == null)
|
||||
if (formInput is null)
|
||||
{
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public partial class NewExtensionPage : ContentPage
|
||||
|
||||
public override IContent[] GetContent()
|
||||
{
|
||||
return _resultForm != null ? [_resultForm] : [_inputForm];
|
||||
return _resultForm is not null ? [_resultForm] : [_inputForm];
|
||||
}
|
||||
|
||||
public NewExtensionPage()
|
||||
@@ -28,13 +28,13 @@ public partial class NewExtensionPage : ContentPage
|
||||
|
||||
private void FormSubmitted(NewExtensionFormBase sender, NewExtensionFormBase? args)
|
||||
{
|
||||
if (_resultForm != null)
|
||||
if (_resultForm is not null)
|
||||
{
|
||||
_resultForm.FormSubmitted -= FormSubmitted;
|
||||
}
|
||||
|
||||
_resultForm = args;
|
||||
if (_resultForm != null)
|
||||
if (_resultForm is not null)
|
||||
{
|
||||
_resultForm.FormSubmitted += FormSubmitted;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using AdaptiveCards.ObjectModel.WinUI3;
|
||||
using AdaptiveCards.Templating;
|
||||
@@ -96,109 +97,40 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
UpdateProperty(nameof(Card));
|
||||
}
|
||||
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveSubmitAction))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveExecuteAction))]
|
||||
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
|
||||
{
|
||||
// BODGY circa GH #40979
|
||||
// Usually, you're supposed to try to cast the action to a specific
|
||||
// type, and use those objects to get the data you need.
|
||||
// However, there's something weird with AdaptiveCards and the way it
|
||||
// works when we consume it when built in Release, with AOT (and
|
||||
// trimming) enabled. Any sort of `action.As<IAdaptiveSubmitAction>()`
|
||||
// or similar will throw a System.InvalidCastException.
|
||||
//
|
||||
// Instead we have this horror show.
|
||||
//
|
||||
// The `action.ToJson()` blob ACTUALLY CONTAINS THE `type` field, which
|
||||
// we can use to determine what kind of action it is. Then we can parse
|
||||
// the JSON manually based on the type.
|
||||
var actionJson = action.ToJson();
|
||||
|
||||
if (actionJson.TryGetValue("type", out var actionTypeValue))
|
||||
if (action is AdaptiveOpenUrlAction openUrlAction)
|
||||
{
|
||||
var actionTypeString = actionTypeValue.GetString();
|
||||
Logger.LogTrace($"atString={actionTypeString}");
|
||||
|
||||
var actionType = actionTypeString switch
|
||||
{
|
||||
"Action.Submit" => ActionType.Submit,
|
||||
"Action.Execute" => ActionType.Execute,
|
||||
"Action.OpenUrl" => ActionType.OpenUrl,
|
||||
_ => ActionType.Unsupported,
|
||||
};
|
||||
|
||||
Logger.LogDebug($"{actionTypeString}->{actionType}");
|
||||
|
||||
switch (actionType)
|
||||
{
|
||||
case ActionType.OpenUrl:
|
||||
{
|
||||
HandleOpenUrlAction(action, actionJson);
|
||||
}
|
||||
|
||||
break;
|
||||
case ActionType.Submit:
|
||||
case ActionType.Execute:
|
||||
{
|
||||
HandleSubmitAction(action, actionJson, inputs);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
Logger.LogError($"{actionType} was an unexpected action `type`");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"actionJson.TryGetValue(type) failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleOpenUrlAction(IAdaptiveActionElement action, JsonObject actionJson)
|
||||
{
|
||||
if (actionJson.TryGetValue("url", out var actionUrlValue))
|
||||
{
|
||||
var actionUrl = actionUrlValue.GetString() ?? string.Empty;
|
||||
if (Uri.TryCreate(actionUrl, default(UriCreationOptions), out var uri))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(uri));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Failed to produce URI for {actionUrlValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSubmitAction(
|
||||
IAdaptiveActionElement action,
|
||||
JsonObject actionJson,
|
||||
JsonObject inputs)
|
||||
{
|
||||
var dataString = string.Empty;
|
||||
if (actionJson.TryGetValue("data", out var actionDataValue))
|
||||
{
|
||||
dataString = actionDataValue.Stringify() ?? string.Empty;
|
||||
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
|
||||
return;
|
||||
}
|
||||
|
||||
var inputString = inputs.Stringify();
|
||||
_ = Task.Run(() =>
|
||||
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
|
||||
{
|
||||
try
|
||||
// Get the data and inputs
|
||||
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
|
||||
var inputString = inputs.Stringify();
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
var model = _formModel.Unsafe!;
|
||||
if (model != null)
|
||||
try
|
||||
{
|
||||
var result = model.SubmitForm(inputString, dataString);
|
||||
Logger.LogDebug($"SubmitForm() returned {result}");
|
||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||
var model = _formModel.Unsafe!;
|
||||
if (model != null)
|
||||
{
|
||||
var result = model.SubmitForm(inputString, dataString);
|
||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
});
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string ErrorCardJson = """
|
||||
|
||||
@@ -20,7 +20,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
|
||||
protected void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public partial class ContentMarkdownViewModel(IMarkdownContent _markdown, WeakRe
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
var model = Model.Unsafe;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var root = model.RootContent;
|
||||
if (root != null)
|
||||
if (root is not null)
|
||||
{
|
||||
RootContent = ViewModelFromContent(root, PageContext);
|
||||
RootContent?.InitializeProperties();
|
||||
@@ -82,7 +82,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
||||
protected void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
@@ -91,7 +91,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
||||
{
|
||||
case nameof(RootContent):
|
||||
var root = model.RootContent;
|
||||
if (root != null)
|
||||
if (root is not null)
|
||||
{
|
||||
RootContent = ViewModelFromContent(root, PageContext);
|
||||
}
|
||||
@@ -119,7 +119,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
var viewModel = ViewModelFromContent(item, PageContext);
|
||||
if (viewModel != null)
|
||||
if (viewModel is not null)
|
||||
{
|
||||
viewModel.InitializeProperties();
|
||||
newContent.Add(viewModel);
|
||||
@@ -153,7 +153,7 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
||||
|
||||
Children.Clear();
|
||||
var model = Model.Unsafe;
|
||||
if (model != null)
|
||||
if (model is not null)
|
||||
{
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -90,7 +90,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
||||
}).Result;
|
||||
var isExtension = isCmdPalExtensionResult.IsExtension;
|
||||
var extension = isCmdPalExtensionResult.Extension;
|
||||
if (isExtension && extension != null)
|
||||
if (isExtension && extension is not null)
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
if (cmdPalProvider == null || classIds.Count == 0)
|
||||
if (cmdPalProvider is null || classIds.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
@@ -352,12 +352,12 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
||||
{
|
||||
var propSetList = new List<string>();
|
||||
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
|
||||
if (singlePropertySet != null)
|
||||
if (singlePropertySet is not null)
|
||||
{
|
||||
var classId = GetProperty(singlePropertySet, ClassIdProperty);
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -365,7 +365,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
||||
else
|
||||
{
|
||||
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
|
||||
if (propertySetArray != null)
|
||||
if (propertySetArray is not null)
|
||||
{
|
||||
foreach (var prop in propertySetArray)
|
||||
{
|
||||
@@ -375,7 +375,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
|
||||
}
|
||||
|
||||
var classId = GetProperty(propertySet, ClassIdProperty);
|
||||
if (classId != null)
|
||||
if (classId is not null)
|
||||
{
|
||||
propSetList.Add(classId);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ProviderSettings
|
||||
public void Connect(CommandProviderWrapper wrapper)
|
||||
{
|
||||
ProviderId = wrapper.ProviderId;
|
||||
IsBuiltin = wrapper.Extension == null;
|
||||
IsBuiltin = wrapper.Extension is null;
|
||||
|
||||
ProviderDisplayName = wrapper.DisplayName;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public partial class ProviderSettingsViewModel(
|
||||
Resources.builtin_disabled_extension;
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Extension))]
|
||||
public bool IsFromExtension => _provider.Extension != null;
|
||||
public bool IsFromExtension => _provider.Extension is not null;
|
||||
|
||||
public IExtensionWrapper? Extension => _provider.Extension;
|
||||
|
||||
@@ -76,7 +76,7 @@ public partial class ProviderSettingsViewModel(
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
if (_provider.Settings is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -100,7 +100,7 @@ public partial class ProviderSettingsViewModel(
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
if (_provider.Settings is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -126,7 +126,7 @@ public partial class ProviderSettingsViewModel(
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field == null)
|
||||
if (field is null)
|
||||
{
|
||||
field = BuildTopLevelViewModels();
|
||||
}
|
||||
@@ -149,7 +149,7 @@ public partial class ProviderSettingsViewModel(
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field == null)
|
||||
if (field is null)
|
||||
{
|
||||
field = BuildFallbackViewModels();
|
||||
}
|
||||
@@ -173,7 +173,7 @@ public partial class ProviderSettingsViewModel(
|
||||
|
||||
private void InitializeSettingsPage()
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
if (_provider.Settings is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public partial class RecentCommandsManager : ObservableObject
|
||||
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
|
||||
// match after one use.
|
||||
// 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;
|
||||
|
||||
@@ -61,7 +61,7 @@ public partial class RecentCommandsManager : ObservableObject
|
||||
var entry = History
|
||||
.Where(item => item.CommandId == commandId)
|
||||
.FirstOrDefault();
|
||||
if (entry == null)
|
||||
if (entry is null)
|
||||
{
|
||||
var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 };
|
||||
History.Insert(0, newitem);
|
||||
|
||||
@@ -95,7 +95,7 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
_extensionCommandProviders.Clear();
|
||||
}
|
||||
|
||||
if (extensions != null)
|
||||
if (extensions is not null)
|
||||
{
|
||||
await StartExtensionsAndGetCommands(extensions);
|
||||
}
|
||||
@@ -283,7 +283,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
var startTasks = extensions.Select(StartExtensionWithTimeoutAsync);
|
||||
|
||||
// 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)
|
||||
{
|
||||
@@ -293,7 +293,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
// Load the commands from the providers in parallel
|
||||
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)
|
||||
{
|
||||
@@ -410,8 +410,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
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";
|
||||
CommandPaletteHost.Instance.Log(errorMessage);
|
||||
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager");
|
||||
CommandPaletteHost.Instance.Log(message);
|
||||
}
|
||||
|
||||
internal bool IsProviderActive(string id)
|
||||
|
||||
@@ -240,7 +240,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
private void FetchAliasFromAliasManager()
|
||||
{
|
||||
var am = _serviceProvider.GetService<AliasManager>();
|
||||
if (am != null)
|
||||
if (am is not null)
|
||||
{
|
||||
var commandAlias = am.AliasFromId(Id);
|
||||
if (commandAlias is not null)
|
||||
@@ -254,7 +254,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
private void UpdateHotkey()
|
||||
{
|
||||
var hotkey = _settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault();
|
||||
if (hotkey != null)
|
||||
if (hotkey is not null)
|
||||
{
|
||||
_hotkey = hotkey.Hotkey;
|
||||
}
|
||||
@@ -264,12 +264,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
{
|
||||
List<Tag> tags = [];
|
||||
|
||||
if (Hotkey != null)
|
||||
if (Hotkey is not null)
|
||||
{
|
||||
tags.Add(new Tag() { Text = Hotkey.ToString() });
|
||||
}
|
||||
|
||||
if (Alias != null)
|
||||
if (Alias is not null)
|
||||
{
|
||||
tags.Add(new Tag() { Text = Alias.SearchPrefix });
|
||||
}
|
||||
|
||||
@@ -41,13 +41,18 @@
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened">
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||
</Flyout>
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ using Microsoft.CmdPal.UI.Views;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
@@ -50,7 +49,7 @@ public sealed partial class CommandBar : UserControl,
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Element == null)
|
||||
if (message.Element is null)
|
||||
{
|
||||
_ = 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,
|
||||
// then the second call will explode, because "CardOverrideStyles is already the child of another element".
|
||||
// SO only set this once.
|
||||
if (_renderer.OverrideStyles == null)
|
||||
if (_renderer.OverrideStyles is null)
|
||||
{
|
||||
_renderer.OverrideStyles = CardOverrideStyles;
|
||||
}
|
||||
@@ -55,19 +55,19 @@ public sealed partial class ContentFormControl : UserControl
|
||||
|
||||
private void AttachViewModel(ContentFormViewModel? vm)
|
||||
{
|
||||
if (_viewModel != null)
|
||||
if (_viewModel is not null)
|
||||
{
|
||||
_viewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
_viewModel = vm;
|
||||
|
||||
if (_viewModel != null)
|
||||
if (_viewModel is not null)
|
||||
{
|
||||
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
var c = _viewModel.Card;
|
||||
if (c != null)
|
||||
if (c is not null)
|
||||
{
|
||||
DisplayCard(c);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ public sealed partial class ContentFormControl : UserControl
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null)
|
||||
if (ViewModel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public sealed partial class ContentFormControl : UserControl
|
||||
if (e.PropertyName == nameof(ViewModel.Card))
|
||||
{
|
||||
var c = ViewModel.Card;
|
||||
if (c != null)
|
||||
if (c is not null)
|
||||
{
|
||||
DisplayCard(c);
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public sealed partial class ContentFormControl : UserControl
|
||||
{
|
||||
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
|
||||
ContentGrid.Children.Clear();
|
||||
if (_renderedCard.FrameworkElement != null)
|
||||
if (_renderedCard.FrameworkElement is not null)
|
||||
{
|
||||
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
|
||||
|
||||
@@ -148,7 +148,7 @@ public sealed partial class ContentFormControl : UserControl
|
||||
|
||||
// Recursively check children
|
||||
var result = FindFirstFocusableElement(child);
|
||||
if (result != null)
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed partial class ContextMenu : UserControl,
|
||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||
|
||||
if (ViewModel != null)
|
||||
if (ViewModel is not null)
|
||||
{
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public partial class IconBox : ContentControl
|
||||
{
|
||||
if (d is IconBox @this)
|
||||
{
|
||||
if (e.NewValue == null)
|
||||
if (e.NewValue is null)
|
||||
{
|
||||
@this.Source = null;
|
||||
}
|
||||
@@ -104,7 +104,7 @@ public partial class IconBox : ContentControl
|
||||
var requestedTheme = @this.ActualTheme;
|
||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||
|
||||
if (@this.SourceRequested != null)
|
||||
if (@this.SourceRequested is not null)
|
||||
{
|
||||
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
|
||||
|
||||
@@ -142,7 +142,7 @@ public partial class IconBox : ContentControl
|
||||
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark;
|
||||
}
|
||||
|
||||
if (iconData != null &&
|
||||
if (iconData is not null &&
|
||||
@this.Source is FontIconSource)
|
||||
{
|
||||
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.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
@@ -80,12 +79,12 @@ public sealed partial class KeyVisual : Control
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_keyVisual == null)
|
||||
if (_keyVisual is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_keyVisual.Content != null)
|
||||
if (_keyVisual.Content is not null)
|
||||
{
|
||||
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...
|
||||
var @this = (SearchBar)d;
|
||||
|
||||
if (@this != null
|
||||
if (@this is not null
|
||||
&& e.OldValue is PageViewModel old)
|
||||
{
|
||||
old.PropertyChanged -= @this.Page_PropertyChanged;
|
||||
}
|
||||
|
||||
if (@this != null
|
||||
if (@this is not null
|
||||
&& e.NewValue is PageViewModel page)
|
||||
{
|
||||
// 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;
|
||||
|
||||
if (CurrentPageViewModel != null)
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = string.Empty;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
FilterBox.Text = string.Empty;
|
||||
|
||||
// hack TODO GH #245
|
||||
if (CurrentPageViewModel != null)
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||
}
|
||||
@@ -154,7 +154,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
else if (e.Key == VirtualKey.Back)
|
||||
{
|
||||
// hack TODO GH #245
|
||||
if (CurrentPageViewModel != null)
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||
}
|
||||
@@ -318,7 +318,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
}
|
||||
|
||||
// Actually plumb Filtering to the view model
|
||||
if (CurrentPageViewModel != null)
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||
}
|
||||
|
||||
@@ -39,13 +39,13 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
||||
private static void OnAllowDisableChanged(DependencyObject d, DependencyPropertyChangedEventArgs? e)
|
||||
{
|
||||
var me = d as ShortcutControl;
|
||||
if (me == null)
|
||||
if (me is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var description = me.c?.FindDescendant<TextBlock>();
|
||||
if (description == null)
|
||||
if (description is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -431,7 +431,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
||||
|
||||
private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
if (lastValidSettings != null && ComboIsValid(lastValidSettings))
|
||||
if (lastValidSettings is not null && ComboIsValid(lastValidSettings))
|
||||
{
|
||||
HotkeySettings = lastValidSettings with { };
|
||||
}
|
||||
@@ -458,7 +458,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
||||
|
||||
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);
|
||||
@@ -466,12 +466,12 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
||||
private void DoWindowActivated(WindowActivatedEventArgs args)
|
||||
{
|
||||
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.
|
||||
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.
|
||||
hook.Dispose();
|
||||
@@ -490,7 +490,7 @@ public sealed partial class ShortcutControl : UserControl, IDisposable, IRecipie
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (hook != null)
|
||||
if (hook is not null)
|
||||
{
|
||||
hook.Dispose();
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public partial class Tag : Control
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag.ForegroundColor != null &&
|
||||
if (tag.ForegroundColor is not null &&
|
||||
OptionalColorBrushCacheProvider.Convert(tag.ForegroundColor.Value) is SolidColorBrush brush)
|
||||
{
|
||||
tag.Foreground = brush;
|
||||
@@ -114,7 +114,7 @@ public partial class Tag : Control
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag.BackgroundColor != null &&
|
||||
if (tag.BackgroundColor is not null &&
|
||||
OptionalColorBrushCacheProvider.Convert(tag.BackgroundColor.Value) is SolidColorBrush brush)
|
||||
{
|
||||
tag.Background = brush;
|
||||
|
||||
@@ -34,8 +34,14 @@ public sealed partial class ContentPage : Page,
|
||||
public ContentPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
|
||||
this.Unloaded += OnUnloaded;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -45,6 +51,16 @@ public sealed partial class ContentPage : Page,
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
@@ -24,6 +25,8 @@ public sealed partial class ListPage : Page,
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
IRecipient<ActivateSecondaryCommandMessage>
|
||||
{
|
||||
private InputSource _lastInputSource;
|
||||
|
||||
private ListViewModel? ViewModel
|
||||
{
|
||||
get => (ListViewModel?)GetValue(ViewModelProperty);
|
||||
@@ -39,6 +42,8 @@ public sealed partial class ListPage : Page,
|
||||
this.InitializeComponent();
|
||||
this.NavigationCacheMode = NavigationCacheMode.Disabled;
|
||||
this.ItemsList.Loaded += ItemsList_Loaded;
|
||||
this.ItemsList.PreviewKeyDown += ItemsList_PreviewKeyDown;
|
||||
this.ItemsList.PointerPressed += ItemsList_PointerPressed;
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
@@ -74,7 +79,7 @@ public sealed partial class ListPage : Page,
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
if (ViewModel != null)
|
||||
if (ViewModel is not null)
|
||||
{
|
||||
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
||||
ViewModel.ItemsUpdated -= Page_ItemsUpdated;
|
||||
@@ -98,6 +103,12 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
if (e.ClickedItem is ListItemViewModel item)
|
||||
{
|
||||
if (_lastInputSource == InputSource.Keyboard)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
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
|
||||
// it's in the list (otherwise, clear the cache), but that seems
|
||||
// aggressively BODGY for something that mostly just works today.
|
||||
if (ItemsList.SelectedItem != null)
|
||||
if (ItemsList.SelectedItem is not null)
|
||||
{
|
||||
ItemsList.ScrollIntoView(ItemsList.SelectedItem);
|
||||
|
||||
// Automation notification for screen readers
|
||||
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;
|
||||
listViewPeer.RaiseNotificationEvent(
|
||||
@@ -165,7 +176,7 @@ public sealed partial class ListPage : Page,
|
||||
// Find the ScrollViewer in the ListView
|
||||
var listViewScrollViewer = FindScrollViewer(this.ItemsList);
|
||||
|
||||
if (listViewScrollViewer != null)
|
||||
if (listViewScrollViewer is not null)
|
||||
{
|
||||
listViewScrollViewer.ViewChanged += ListViewScrollViewer_ViewChanged;
|
||||
}
|
||||
@@ -174,7 +185,7 @@ public sealed partial class ListPage : Page,
|
||||
private void ListViewScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e)
|
||||
{
|
||||
var scrollView = sender as ScrollViewer;
|
||||
if (scrollView == null)
|
||||
if (scrollView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -256,7 +267,7 @@ public sealed partial class ListPage : Page,
|
||||
page.PropertyChanged += @this.ViewModel_PropertyChanged;
|
||||
page.ItemsUpdated += @this.Page_ItemsUpdated;
|
||||
}
|
||||
else if (e.NewValue == null)
|
||||
else if (e.NewValue is null)
|
||||
{
|
||||
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
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
if (ItemsList.SelectedItem == null)
|
||||
if (ItemsList.SelectedItem is null)
|
||||
{
|
||||
ItemsList.SelectedIndex = 0;
|
||||
}
|
||||
@@ -307,7 +318,7 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
var result = FindScrollViewer(child);
|
||||
if (result != null)
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@@ -329,7 +340,7 @@ public sealed partial class ListPage : Page,
|
||||
_ => (null, null),
|
||||
};
|
||||
|
||||
if (item == null || element == null)
|
||||
if (item is null || element is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -363,4 +374,21 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
_ = 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.
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
@@ -63,7 +56,7 @@ internal static class GpoValueChecker
|
||||
{
|
||||
using (RegistryKey? key = rootKey.OpenSubKey(subKeyPath, false))
|
||||
{
|
||||
if (key == null)
|
||||
if (key is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using 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)
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
{
|
||||
if (args.Key == null)
|
||||
if (args.Key is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false);
|
||||
return source;
|
||||
}
|
||||
else if (icon.Data != null)
|
||||
else if (icon.Data is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -49,7 +49,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
||||
|
||||
private async Task<IconSource?> StreamToIconSource(IRandomAccessStreamReference iconStreamRef)
|
||||
{
|
||||
if (iconStreamRef == null)
|
||||
if (iconStreamRef is 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;
|
||||
}
|
||||
@@ -50,7 +50,7 @@ internal sealed partial class TrayIconService
|
||||
{
|
||||
if (showSystemTrayIcon ?? _settingsModel.ShowSystemTrayIcon)
|
||||
{
|
||||
if (_window == null)
|
||||
if (_window is null)
|
||||
{
|
||||
_window = new Window();
|
||||
_hwnd = new HWND(WindowNative.GetWindowHandle(_window));
|
||||
@@ -64,7 +64,7 @@ internal sealed partial class TrayIconService
|
||||
_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
|
||||
// explorer restarts, we'll come back through here, and we don't
|
||||
@@ -88,7 +88,7 @@ internal sealed partial class TrayIconService
|
||||
// Add the notification icon
|
||||
PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d);
|
||||
|
||||
if (_popupMenu == null)
|
||||
if (_popupMenu is null)
|
||||
{
|
||||
_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"));
|
||||
@@ -103,7 +103,7 @@ internal sealed partial class TrayIconService
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (_trayIconData != null)
|
||||
if (_trayIconData is not null)
|
||||
{
|
||||
var d = (NOTIFYICONDATAW)_trayIconData;
|
||||
if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d))
|
||||
@@ -112,19 +112,19 @@ internal sealed partial class TrayIconService
|
||||
}
|
||||
}
|
||||
|
||||
if (_popupMenu != null)
|
||||
if (_popupMenu is not null)
|
||||
{
|
||||
_popupMenu.Close();
|
||||
_popupMenu = null;
|
||||
}
|
||||
|
||||
if (_largeIcon != null)
|
||||
if (_largeIcon is not null)
|
||||
{
|
||||
_largeIcon.Close();
|
||||
_largeIcon = null;
|
||||
}
|
||||
|
||||
if (_window != null)
|
||||
if (_window is not null)
|
||||
{
|
||||
_window.Close();
|
||||
_window = null;
|
||||
@@ -167,7 +167,7 @@ internal sealed partial class TrayIconService
|
||||
// WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
|
||||
case PInvoke.WM_WINDOWPOSCHANGING:
|
||||
{
|
||||
if (_trayIconData == null)
|
||||
if (_trayIconData is null)
|
||||
{
|
||||
SetupTrayIcon();
|
||||
}
|
||||
@@ -189,7 +189,7 @@ internal sealed partial class TrayIconService
|
||||
{
|
||||
case PInvoke.WM_RBUTTONUP:
|
||||
{
|
||||
if (_popupMenu != null)
|
||||
if (_popupMenu is not null)
|
||||
{
|
||||
PInvoke.GetCursorPos(out var cursorPos);
|
||||
PInvoke.SetForegroundWindow(_hwnd);
|
||||
|
||||
@@ -46,7 +46,7 @@ public static class TypedEventHandlerExtensions
|
||||
#pragma warning restore CA1715 // Identifiers should have correct prefix
|
||||
where R : DeferredEventArgs
|
||||
{
|
||||
if (eventHandler == null)
|
||||
if (eventHandler is null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed partial class WindowHelper
|
||||
UserNotificationState 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 ||
|
||||
state == UserNotificationState.QUNS_BUSY ||
|
||||
|
||||
@@ -27,6 +27,7 @@ using Microsoft.Windows.AppLifecycle;
|
||||
using Windows.ApplicationModel.Activation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
using Windows.UI.WindowManagement;
|
||||
using Windows.Win32;
|
||||
@@ -44,7 +45,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<DismissMessage>,
|
||||
IRecipient<ShowWindowMessage>,
|
||||
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", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
||||
@@ -54,6 +56,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private readonly WNDPROC? _originalWndProc;
|
||||
private readonly List<TopLevelHotkey> _hotkeys = [];
|
||||
private readonly KeyboardListener _keyboardListener;
|
||||
private readonly LocalKeyboardListener _localKeyboardListener;
|
||||
private bool _ignoreHotKeyWhenFullScreen = true;
|
||||
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
@@ -116,6 +119,18 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
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();
|
||||
@@ -376,6 +391,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
|
||||
// Workaround by turning it off before shutdown.
|
||||
App.Current.DebugSettings.FailFastOnErrors = false;
|
||||
_localKeyboardListener.Dispose();
|
||||
DisposeAcrylic();
|
||||
|
||||
_keyboardListener.Stop();
|
||||
@@ -384,7 +400,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void DisposeAcrylic()
|
||||
{
|
||||
if (_acrylicController != null)
|
||||
if (_acrylicController is not null)
|
||||
{
|
||||
_acrylicController.Dispose();
|
||||
_acrylicController = null!;
|
||||
@@ -459,7 +475,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
|
||||
}
|
||||
|
||||
if (_configurationSource != null)
|
||||
if (_configurationSource is not null)
|
||||
{
|
||||
_configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
|
||||
}
|
||||
@@ -467,7 +483,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
|
||||
{
|
||||
if (activatedEventArgs == null)
|
||||
if (activatedEventArgs is null)
|
||||
{
|
||||
Summon(string.Empty);
|
||||
return;
|
||||
@@ -535,7 +551,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
UnregisterHotkeys();
|
||||
|
||||
var globalHotkey = settings.Hotkey;
|
||||
if (globalHotkey != null)
|
||||
if (globalHotkey is not null)
|
||||
{
|
||||
if (settings.UseLowLevelGlobalHotkey)
|
||||
{
|
||||
@@ -565,7 +581,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
var key = commandHotkey.Hotkey;
|
||||
|
||||
if (key != null)
|
||||
if (key is not null)
|
||||
{
|
||||
if (settings.UseLowLevelGlobalHotkey)
|
||||
{
|
||||
@@ -682,4 +698,10 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_localKeyboardListener.Dispose();
|
||||
DisposeAcrylic();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,4 +47,10 @@ DWM_CLOAKED_APP
|
||||
|
||||
CoWaitForMultipleObjects
|
||||
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)
|
||||
{
|
||||
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.
|
||||
// Once the mainpage loads, we'll start loading extensions.
|
||||
|
||||
@@ -171,7 +171,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
// This gets called from the UI thread
|
||||
private async Task HandleConfirmArgsOnUiThread(IConfirmationArgs? args)
|
||||
{
|
||||
if (args == null)
|
||||
if (args is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -236,7 +236,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public void OpenSettings()
|
||||
{
|
||||
if (_settingsWindow == null)
|
||||
if (_settingsWindow is null)
|
||||
{
|
||||
_settingsWindow = new SettingsWindow();
|
||||
}
|
||||
@@ -324,7 +324,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
// command from our list of toplevel commands.
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
var topLevelCommand = tlcManager.LookupCommand(commandId);
|
||||
if (topLevelCommand != null)
|
||||
if (topLevelCommand is not null)
|
||||
{
|
||||
var command = topLevelCommand.CommandViewModel.Model.Unsafe;
|
||||
var isPage = command is not IInvokableCommand;
|
||||
|
||||
@@ -100,7 +100,7 @@ internal sealed class PowerToysRootPageService : IRootPageService
|
||||
_activeExtension = extension;
|
||||
|
||||
var extensionWinRtObject = _activeExtension?.GetExtensionObject();
|
||||
if (extensionWinRtObject != null)
|
||||
if (extensionWinRtObject is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -105,6 +105,8 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
private void Window_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<SettingsWindowClosedMessage>();
|
||||
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
}
|
||||
|
||||
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class AllAppsCommandProviderTests : AppsTestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public void ProviderHasDisplayName()
|
||||
{
|
||||
// Setup
|
||||
var provider = new AllAppsCommandProvider();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(provider.DisplayName);
|
||||
Assert.IsTrue(provider.DisplayName.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderHasIcon()
|
||||
{
|
||||
// Setup
|
||||
var provider = new AllAppsCommandProvider();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(provider.Icon);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TopLevelCommandsNotEmpty()
|
||||
{
|
||||
// Setup
|
||||
var provider = new AllAppsCommandProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
Assert.IsTrue(commands.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LookupAppWithEmptyNameReturnsNotNull()
|
||||
{
|
||||
// Setup
|
||||
var mockApp = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
|
||||
MockCache.AddWin32Program(mockApp);
|
||||
var page = new AllAppsPage(MockCache);
|
||||
|
||||
var provider = new AllAppsCommandProvider(page);
|
||||
|
||||
// Act
|
||||
var result = provider.LookupApp(string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProviderWithMockData_LookupApp_ReturnsCorrectApp()
|
||||
{
|
||||
// Arrange
|
||||
var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
|
||||
MockCache.AddWin32Program(testApp);
|
||||
|
||||
var provider = new AllAppsCommandProvider(Page);
|
||||
|
||||
// Wait for initialization to complete
|
||||
await WaitForPageInitializationAsync();
|
||||
|
||||
// Act
|
||||
var result = provider.LookupApp("TestApp");
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual("TestApp", result.Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ProviderWithMockData_LookupApp_ReturnsNullForNonExistentApp()
|
||||
{
|
||||
// Arrange
|
||||
var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
|
||||
MockCache.AddWin32Program(testApp);
|
||||
|
||||
var provider = new AllAppsCommandProvider(Page);
|
||||
|
||||
// Wait for initialization to complete
|
||||
await WaitForPageInitializationAsync();
|
||||
|
||||
// Act
|
||||
var result = provider.LookupApp("NonExistentApp");
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderWithMockData_TopLevelCommands_IncludesListItem()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new AllAppsCommandProvider(Page);
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
Assert.IsTrue(commands.Length >= 1); // At least the list item should be present
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class AllAppsPageTests : AppsTestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public void AllAppsPage_Constructor_ThrowsOnNullAppCache()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new AllAppsPage(null!));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AllAppsPage_WithMockCache_InitializesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var mockCache = new MockAppCache();
|
||||
|
||||
// Act
|
||||
var page = new AllAppsPage(mockCache);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(page);
|
||||
Assert.IsNotNull(page.Name);
|
||||
Assert.IsNotNull(page.Icon);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AllAppsPage_GetItems_ReturnsEmptyWithEmptyCache()
|
||||
{
|
||||
// Act - Wait for initialization to complete
|
||||
await WaitForPageInitializationAsync();
|
||||
var items = Page.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(items);
|
||||
Assert.AreEqual(0, items.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AllAppsPage_GetItems_ReturnsAppsFromCacheAsync()
|
||||
{
|
||||
// Arrange
|
||||
var mockCache = new MockAppCache();
|
||||
var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
|
||||
var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
|
||||
|
||||
mockCache.AddWin32Program(win32App);
|
||||
mockCache.AddUWPApplication(uwpApp);
|
||||
|
||||
var page = new AllAppsPage(mockCache);
|
||||
|
||||
// Wait a bit for initialization to complete
|
||||
await Task.Delay(100);
|
||||
|
||||
// Act
|
||||
var items = page.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(items);
|
||||
Assert.AreEqual(2, items.Length);
|
||||
|
||||
// we need to loop the items to ensure we got the correct ones
|
||||
Assert.IsTrue(items.Any(i => i.Title == "Notepad"));
|
||||
Assert.IsTrue(items.Any(i => i.Title == "Calculator"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AllAppsPage_GetPinnedApps_ReturnsEmptyWhenNoAppsArePinned()
|
||||
{
|
||||
// Arrange
|
||||
var mockCache = new MockAppCache();
|
||||
var app = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
|
||||
mockCache.AddWin32Program(app);
|
||||
|
||||
var page = new AllAppsPage(mockCache);
|
||||
|
||||
// Wait a bit for initialization to complete
|
||||
await Task.Delay(100);
|
||||
|
||||
// Act
|
||||
var pinnedApps = page.GetPinnedApps();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(pinnedApps);
|
||||
Assert.AreEqual(0, pinnedApps.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for Apps unit tests that provides common setup and teardown functionality.
|
||||
/// </summary>
|
||||
public abstract class AppsTestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the mock application cache used in tests.
|
||||
/// </summary>
|
||||
protected MockAppCache MockCache { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AllAppsPage instance used in tests.
|
||||
/// </summary>
|
||||
protected AllAppsPage Page { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the test environment before each test method.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous setup operation.</returns>
|
||||
[TestInitialize]
|
||||
public virtual async Task Setup()
|
||||
{
|
||||
MockCache = new MockAppCache();
|
||||
Page = new AllAppsPage(MockCache);
|
||||
|
||||
// Ensure initialization is complete
|
||||
await MockCache.RefreshAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up the test environment after each test method.
|
||||
/// </summary>
|
||||
[TestCleanup]
|
||||
public virtual void Cleanup()
|
||||
{
|
||||
MockCache?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces synchronous initialization of the page for testing.
|
||||
/// </summary>
|
||||
protected void EnsurePageInitialized()
|
||||
{
|
||||
// Trigger BuildListItems by accessing items
|
||||
_ = Page.GetItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for page initialization with timeout.
|
||||
/// </summary>
|
||||
/// <param name="timeoutMs">The timeout in milliseconds.</param>
|
||||
/// <returns>A task representing the asynchronous wait operation.</returns>
|
||||
protected async Task WaitForPageInitializationAsync(int timeoutMs = 1000)
|
||||
{
|
||||
await MockCache.RefreshAsync();
|
||||
EnsurePageInitialized();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Apps.UnitTests</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Mock implementation of IAppCache for unit testing.
|
||||
/// </summary>
|
||||
public class MockAppCache : IAppCache
|
||||
{
|
||||
private readonly List<Win32Program> _win32s = new();
|
||||
private readonly List<IUWPApplication> _uwps = new();
|
||||
private bool _disposed;
|
||||
private bool _shouldReload;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of Win32 programs.
|
||||
/// </summary>
|
||||
public IList<Win32Program> Win32s => _win32s.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of UWP applications.
|
||||
/// </summary>
|
||||
public IList<IUWPApplication> UWPs => _uwps.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the cache should be reloaded.
|
||||
/// </summary>
|
||||
/// <returns>True if cache should be reloaded, false otherwise.</returns>
|
||||
public bool ShouldReload() => _shouldReload;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the reload flag.
|
||||
/// </summary>
|
||||
public void ResetReloadFlag() => _shouldReload = false;
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously refreshes the cache.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous refresh operation.</returns>
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
// Simulate minimal async operation for testing
|
||||
await Task.Delay(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a Win32 program to the cache.
|
||||
/// </summary>
|
||||
/// <param name="program">The Win32 program to add.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when program is null.</exception>
|
||||
public void AddWin32Program(Win32Program program)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(program);
|
||||
|
||||
_win32s.Add(program);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a UWP application to the cache.
|
||||
/// </summary>
|
||||
/// <param name="app">The UWP application to add.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when app is null.</exception>
|
||||
public void AddUWPApplication(IUWPApplication app)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
_uwps.Add(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all applications from the cache.
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
_win32s.Clear();
|
||||
_uwps.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Clean up managed resources
|
||||
_win32s.Clear();
|
||||
_uwps.Clear();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Mock implementation of IUWPApplication for unit testing.
|
||||
/// </summary>
|
||||
public class MockUWPApplication : IUWPApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the app list entry.
|
||||
/// </summary>
|
||||
public string AppListEntry { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier.
|
||||
/// </summary>
|
||||
public string UniqueIdentifier { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display name.
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user model ID.
|
||||
/// </summary>
|
||||
public string UserModelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the background color.
|
||||
/// </summary>
|
||||
public string BackgroundColor { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entry point.
|
||||
/// </summary>
|
||||
public string EntryPoint { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the application is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the application can run elevated.
|
||||
/// </summary>
|
||||
public bool CanRunElevated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logo path.
|
||||
/// </summary>
|
||||
public string LogoPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logo type.
|
||||
/// </summary>
|
||||
public LogoType LogoType { get; set; } = LogoType.Colored;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UWP package.
|
||||
/// </summary>
|
||||
public UWP Package { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the application.
|
||||
/// </summary>
|
||||
public string Name => DisplayName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the location of the application.
|
||||
/// </summary>
|
||||
public string Location => Package?.Location ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized location of the application.
|
||||
/// </summary>
|
||||
public string LocationLocalized => Package?.LocationLocalized ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application identifier.
|
||||
/// </summary>
|
||||
/// <returns>The user model ID of the application.</returns>
|
||||
public string GetAppIdentifier()
|
||||
{
|
||||
return UserModelId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the commands available for this application.
|
||||
/// </summary>
|
||||
/// <returns>A list of context items.</returns>
|
||||
public List<IContextItem> GetCommands()
|
||||
{
|
||||
return new List<IContextItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the logo path based on the specified theme.
|
||||
/// </summary>
|
||||
/// <param name="theme">The theme to use for the logo.</param>
|
||||
public void UpdateLogoPath(Theme theme)
|
||||
{
|
||||
// Mock implementation - no-op for testing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this UWP application to an AppItem.
|
||||
/// </summary>
|
||||
/// <returns>An AppItem representation of this UWP application.</returns>
|
||||
public AppItem ToAppItem()
|
||||
{
|
||||
var iconPath = LogoType != LogoType.Error ? LogoPath : string.Empty;
|
||||
return new AppItem()
|
||||
{
|
||||
Name = Name,
|
||||
Subtitle = Description,
|
||||
Type = "Packaged Application", // Equivalent to UWPApplication.Type()
|
||||
IcoPath = iconPath,
|
||||
DirPath = Location,
|
||||
UserModelId = UserModelId,
|
||||
IsPackaged = true,
|
||||
Commands = GetCommands(),
|
||||
AppIdentifier = GetAppIdentifier(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public void QueryReturnsExpectedResults()
|
||||
{
|
||||
// Arrange
|
||||
var mockCache = new MockAppCache();
|
||||
var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
|
||||
var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
|
||||
mockCache.AddWin32Program(win32App);
|
||||
mockCache.AddUWPApplication(uwpApp);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
mockCache.AddWin32Program(TestDataHelper.CreateTestWin32Program($"App{i}"));
|
||||
mockCache.AddUWPApplication(TestDataHelper.CreateTestUWPApplication($"UWP App {i}"));
|
||||
}
|
||||
|
||||
var page = new AllAppsPage(mockCache);
|
||||
var provider = new AllAppsCommandProvider(page);
|
||||
|
||||
// Act
|
||||
var allItems = page.GetItems();
|
||||
|
||||
// Assert
|
||||
var notepadResult = Query("notepad", allItems).FirstOrDefault();
|
||||
Assert.IsNotNull(notepadResult);
|
||||
Assert.AreEqual("Notepad", notepadResult.Title);
|
||||
|
||||
var calculatorResult = Query("cal", allItems).FirstOrDefault();
|
||||
Assert.IsNotNull(calculatorResult);
|
||||
Assert.AreEqual("Calculator", calculatorResult.Title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
|
||||
|
||||
public class Settings : ISettingsInterface
|
||||
{
|
||||
private readonly bool enableStartMenuSource;
|
||||
private readonly bool enableDesktopSource;
|
||||
private readonly bool enableRegistrySource;
|
||||
private readonly bool enablePathEnvironmentVariableSource;
|
||||
private readonly List<string> programSuffixes;
|
||||
private readonly List<string> runCommandSuffixes;
|
||||
|
||||
public Settings(
|
||||
bool enableStartMenuSource = true,
|
||||
bool enableDesktopSource = true,
|
||||
bool enableRegistrySource = true,
|
||||
bool enablePathEnvironmentVariableSource = true,
|
||||
List<string> programSuffixes = null,
|
||||
List<string> runCommandSuffixes = null)
|
||||
{
|
||||
this.enableStartMenuSource = enableStartMenuSource;
|
||||
this.enableDesktopSource = enableDesktopSource;
|
||||
this.enableRegistrySource = enableRegistrySource;
|
||||
this.enablePathEnvironmentVariableSource = enablePathEnvironmentVariableSource;
|
||||
this.programSuffixes = programSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url" };
|
||||
this.runCommandSuffixes = runCommandSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url", "cpl", "msc" };
|
||||
}
|
||||
|
||||
public bool EnableStartMenuSource => enableStartMenuSource;
|
||||
|
||||
public bool EnableDesktopSource => enableDesktopSource;
|
||||
|
||||
public bool EnableRegistrySource => enableRegistrySource;
|
||||
|
||||
public bool EnablePathEnvironmentVariableSource => enablePathEnvironmentVariableSource;
|
||||
|
||||
public List<string> ProgramSuffixes => programSuffixes;
|
||||
|
||||
public List<string> RunCommandSuffixes => runCommandSuffixes;
|
||||
|
||||
public static Settings CreateDefaultSettings() => new Settings();
|
||||
|
||||
public static Settings CreateDisabledSourcesSettings() => new Settings(
|
||||
enableStartMenuSource: false,
|
||||
enableDesktopSource: false,
|
||||
enableRegistrySource: false,
|
||||
enablePathEnvironmentVariableSource: false);
|
||||
|
||||
public static Settings CreateCustomSuffixesSettings() => new Settings(
|
||||
programSuffixes: new List<string> { "exe", "bat" },
|
||||
runCommandSuffixes: new List<string> { "exe", "bat", "cmd" });
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to create test data for unit tests.
|
||||
/// </summary>
|
||||
public static class TestDataHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a test Win32 program with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the application.</param>
|
||||
/// <param name="fullPath">The full path to the application executable.</param>
|
||||
/// <param name="enabled">A value indicating whether the application is enabled.</param>
|
||||
/// <param name="valid">A value indicating whether the application is valid.</param>
|
||||
/// <returns>A new Win32Program instance with the specified parameters.</returns>
|
||||
public static Win32Program CreateTestWin32Program(
|
||||
string name = "Test App",
|
||||
string fullPath = "C:\\TestApp\\app.exe",
|
||||
bool enabled = true,
|
||||
bool valid = true)
|
||||
{
|
||||
return new Win32Program
|
||||
{
|
||||
Name = name,
|
||||
FullPath = fullPath,
|
||||
Enabled = enabled,
|
||||
Valid = valid,
|
||||
UniqueIdentifier = $"win32_{name}",
|
||||
Description = $"Test description for {name}",
|
||||
ExecutableName = "app.exe",
|
||||
ParentDirectory = "C:\\TestApp",
|
||||
AppType = Win32Program.ApplicationType.Win32Application,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test UWP application with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="displayName">The display name of the application.</param>
|
||||
/// <param name="userModelId">The user model ID of the application.</param>
|
||||
/// <param name="enabled">A value indicating whether the application is enabled.</param>
|
||||
/// <returns>A new IUWPApplication instance with the specified parameters.</returns>
|
||||
public static IUWPApplication CreateTestUWPApplication(
|
||||
string displayName = "Test UWP App",
|
||||
string userModelId = "TestPublisher.TestUWPApp_1.0.0.0_neutral__8wekyb3d8bbwe",
|
||||
bool enabled = true)
|
||||
{
|
||||
return new MockUWPApplication
|
||||
{
|
||||
DisplayName = displayName,
|
||||
UserModelId = userModelId,
|
||||
Enabled = enabled,
|
||||
UniqueIdentifier = $"uwp_{userModelId}",
|
||||
Description = $"Test UWP description for {displayName}",
|
||||
AppListEntry = "default",
|
||||
BackgroundColor = "#000000",
|
||||
EntryPoint = "TestApp.App",
|
||||
CanRunElevated = false,
|
||||
LogoPath = string.Empty,
|
||||
Package = CreateMockUWPPackage(displayName, userModelId),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock UWP package for testing purposes.
|
||||
/// </summary>
|
||||
/// <param name="displayName">The display name of the package.</param>
|
||||
/// <param name="userModelId">The user model ID of the package.</param>
|
||||
/// <returns>A new UWP package instance.</returns>
|
||||
private static UWP CreateMockUWPPackage(string displayName, string userModelId)
|
||||
{
|
||||
var mockPackage = new MockPackage
|
||||
{
|
||||
Name = displayName,
|
||||
FullName = userModelId,
|
||||
FamilyName = $"{displayName}_8wekyb3d8bbwe",
|
||||
InstalledLocation = $"C:\\Program Files\\WindowsApps\\{displayName}",
|
||||
};
|
||||
|
||||
return new UWP(mockPackage)
|
||||
{
|
||||
Location = mockPackage.InstalledLocation,
|
||||
LocationLocalized = mockPackage.InstalledLocation,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock implementation of IPackage for testing purposes.
|
||||
/// </summary>
|
||||
private sealed class MockPackage : IPackage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the package.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full name of the package.
|
||||
/// </summary>
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the family name of the package.
|
||||
/// </summary>
|
||||
public string FamilyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the package is a framework package.
|
||||
/// </summary>
|
||||
public bool IsFramework { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the package is in development mode.
|
||||
/// </summary>
|
||||
public bool IsDevelopmentMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the installed location of the package.
|
||||
/// </summary>
|
||||
public string InstalledLocation { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class BookmarkDataTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void BookmarkDataWebUrlDetection()
|
||||
{
|
||||
// Act
|
||||
var webBookmark = new BookmarkData
|
||||
{
|
||||
Name = "Test Site",
|
||||
Bookmark = "https://test.com",
|
||||
};
|
||||
|
||||
var nonWebBookmark = new BookmarkData
|
||||
{
|
||||
Name = "Local File",
|
||||
Bookmark = "C:\\temp\\file.txt",
|
||||
};
|
||||
|
||||
var placeholderBookmark = new BookmarkData
|
||||
{
|
||||
Name = "Placeholder",
|
||||
Bookmark = "{Placeholder}",
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(webBookmark.IsWebUrl());
|
||||
Assert.IsFalse(webBookmark.IsPlaceholder);
|
||||
Assert.IsFalse(nonWebBookmark.IsWebUrl());
|
||||
Assert.IsFalse(nonWebBookmark.IsPlaceholder);
|
||||
|
||||
Assert.IsTrue(placeholderBookmark.IsPlaceholder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class BookmarkJsonParserTests
|
||||
{
|
||||
private BookmarkJsonParser _parser;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_parser = new BookmarkJsonParser();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_ValidJson_ReturnsBookmarks()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "Google",
|
||||
"Bookmark": "https://www.google.com"
|
||||
},
|
||||
{
|
||||
"Name": "Local File",
|
||||
"Bookmark": "C:\\temp\\file.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(2, result.Data.Count);
|
||||
Assert.AreEqual("Google", result.Data[0].Name);
|
||||
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
|
||||
Assert.AreEqual("Local File", result.Data[1].Name);
|
||||
Assert.AreEqual("C:\\temp\\file.txt", result.Data[1].Bookmark);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_EmptyJson_ReturnsEmptyBookmarks()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{}";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0, result.Data.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_NullJson_ReturnsEmptyBookmarks()
|
||||
{
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(null);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0, result.Data.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_WhitespaceJson_ReturnsEmptyBookmarks()
|
||||
{
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(" ");
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0, result.Data.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_EmptyString_ReturnsEmptyBookmarks()
|
||||
{
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(string.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0, result.Data.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_InvalidJson_ReturnsEmptyBookmarks()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = "{invalid json}";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(invalidJson);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0, result.Data.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_MalformedJson_ReturnsEmptyBookmarks()
|
||||
{
|
||||
// Arrange
|
||||
var malformedJson = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "Google",
|
||||
"Bookmark": "https://www.google.com"
|
||||
},
|
||||
{
|
||||
"Name": "Incomplete entry"
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(malformedJson);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(0, result.Data.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_JsonWithTrailingCommas_ParsesSuccessfully()
|
||||
{
|
||||
// Arrange - JSON with trailing commas (should be handled by AllowTrailingCommas option)
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "Google",
|
||||
"Bookmark": "https://www.google.com",
|
||||
},
|
||||
{
|
||||
"Name": "Local File",
|
||||
"Bookmark": "C:\\temp\\file.txt",
|
||||
},
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(2, result.Data.Count);
|
||||
Assert.AreEqual("Google", result.Data[0].Name);
|
||||
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_JsonWithDifferentCasing_ParsesSuccessfully()
|
||||
{
|
||||
// Arrange - JSON with different property name casing (should be handled by PropertyNameCaseInsensitive option)
|
||||
var json = """
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "Google",
|
||||
"bookmark": "https://www.google.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.Data.Count);
|
||||
Assert.AreEqual("Google", result.Data[0].Name);
|
||||
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SerializeBookmarks_ValidBookmarks_ReturnsJsonString()
|
||||
{
|
||||
// Arrange
|
||||
var bookmarks = new Bookmarks
|
||||
{
|
||||
Data = new List<BookmarkData>
|
||||
{
|
||||
new BookmarkData { Name = "Google", Bookmark = "https://www.google.com" },
|
||||
new BookmarkData { Name = "Local File", Bookmark = "C:\\temp\\file.txt" },
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _parser.SerializeBookmarks(bookmarks);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsTrue(result.Contains("Google"));
|
||||
Assert.IsTrue(result.Contains("https://www.google.com"));
|
||||
Assert.IsTrue(result.Contains("Local File"));
|
||||
Assert.IsTrue(result.Contains("C:\\\\temp\\\\file.txt")); // Escaped backslashes in JSON
|
||||
Assert.IsTrue(result.Contains("Data"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SerializeBookmarks_EmptyBookmarks_ReturnsValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var bookmarks = new Bookmarks();
|
||||
|
||||
// Act
|
||||
var result = _parser.SerializeBookmarks(bookmarks);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsTrue(result.Contains("Data"));
|
||||
Assert.IsTrue(result.Contains("[]"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SerializeBookmarks_NullBookmarks_ReturnsEmptyString()
|
||||
{
|
||||
// Act
|
||||
var result = _parser.SerializeBookmarks(null);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_RoundTripSerialization_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
var originalBookmarks = new Bookmarks
|
||||
{
|
||||
Data = new List<BookmarkData>
|
||||
{
|
||||
new BookmarkData { Name = "Google", Bookmark = "https://www.google.com" },
|
||||
new BookmarkData { Name = "Local File", Bookmark = "C:\\temp\\file.txt" },
|
||||
new BookmarkData { Name = "Placeholder", Bookmark = "Open {file} in editor" },
|
||||
},
|
||||
};
|
||||
|
||||
// Act - Serialize then parse
|
||||
var serializedJson = _parser.SerializeBookmarks(originalBookmarks);
|
||||
var parsedBookmarks = _parser.ParseBookmarks(serializedJson);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(parsedBookmarks);
|
||||
Assert.AreEqual(originalBookmarks.Data.Count, parsedBookmarks.Data.Count);
|
||||
|
||||
for (var i = 0; i < originalBookmarks.Data.Count; i++)
|
||||
{
|
||||
Assert.AreEqual(originalBookmarks.Data[i].Name, parsedBookmarks.Data[i].Name);
|
||||
Assert.AreEqual(originalBookmarks.Data[i].Bookmark, parsedBookmarks.Data[i].Bookmark);
|
||||
Assert.AreEqual(originalBookmarks.Data[i].IsPlaceholder, parsedBookmarks.Data[i].IsPlaceholder);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_JsonWithPlaceholderBookmarks_CorrectlyIdentifiesPlaceholders()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "Regular URL",
|
||||
"Bookmark": "https://www.google.com"
|
||||
},
|
||||
{
|
||||
"Name": "Placeholder Command",
|
||||
"Bookmark": "notepad {file}"
|
||||
},
|
||||
{
|
||||
"Name": "Multiple Placeholders",
|
||||
"Bookmark": "copy {source} {destination}"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(3, result.Data.Count);
|
||||
|
||||
Assert.IsFalse(result.Data[0].IsPlaceholder);
|
||||
Assert.IsTrue(result.Data[1].IsPlaceholder);
|
||||
Assert.IsTrue(result.Data[2].IsPlaceholder);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_IsWebUrl_CorrectlyIdentifiesWebUrls()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "HTTPS Website",
|
||||
"Bookmark": "https://www.google.com"
|
||||
},
|
||||
{
|
||||
"Name": "HTTP Website",
|
||||
"Bookmark": "http://example.com"
|
||||
},
|
||||
{
|
||||
"Name": "Website without protocol",
|
||||
"Bookmark": "www.github.com"
|
||||
},
|
||||
{
|
||||
"Name": "Local File Path",
|
||||
"Bookmark": "C:\\Users\\test\\Documents\\file.txt"
|
||||
},
|
||||
{
|
||||
"Name": "Network Path",
|
||||
"Bookmark": "\\\\server\\share\\file.txt"
|
||||
},
|
||||
{
|
||||
"Name": "Executable",
|
||||
"Bookmark": "notepad.exe"
|
||||
},
|
||||
{
|
||||
"Name": "File URI",
|
||||
"Bookmark": "file:///C:/temp/file.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(7, result.Data.Count);
|
||||
|
||||
// Web URLs should return true
|
||||
Assert.IsTrue(result.Data[0].IsWebUrl(), "HTTPS URL should be identified as web URL");
|
||||
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTP URL should be identified as web URL");
|
||||
|
||||
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
|
||||
// Assert.IsTrue(result.Data[2].IsWebUrl(), "Domain without protocol should be identified as web URL");
|
||||
|
||||
// Non-web URLs should return false
|
||||
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file path should not be identified as web URL");
|
||||
Assert.IsFalse(result.Data[4].IsWebUrl(), "Network path should not be identified as web URL");
|
||||
Assert.IsFalse(result.Data[5].IsWebUrl(), "Executable should not be identified as web URL");
|
||||
Assert.IsFalse(result.Data[6].IsWebUrl(), "File URI should not be identified as web URL");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_IsPlaceholder_CorrectlyIdentifiesPlaceholders()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "Simple Placeholder",
|
||||
"Bookmark": "notepad {file}"
|
||||
},
|
||||
{
|
||||
"Name": "Multiple Placeholders",
|
||||
"Bookmark": "copy {source} to {destination}"
|
||||
},
|
||||
{
|
||||
"Name": "Web URL with Placeholder",
|
||||
"Bookmark": "https://search.com?q={query}"
|
||||
},
|
||||
{
|
||||
"Name": "Complex Placeholder",
|
||||
"Bookmark": "cmd /c echo {message} > {output_file}"
|
||||
},
|
||||
{
|
||||
"Name": "No Placeholder - Regular URL",
|
||||
"Bookmark": "https://www.google.com"
|
||||
},
|
||||
{
|
||||
"Name": "No Placeholder - Local File",
|
||||
"Bookmark": "C:\\temp\\file.txt"
|
||||
},
|
||||
{
|
||||
"Name": "False Positive - Only Opening Brace",
|
||||
"Bookmark": "test { incomplete"
|
||||
},
|
||||
{
|
||||
"Name": "False Positive - Only Closing Brace",
|
||||
"Bookmark": "test } incomplete"
|
||||
},
|
||||
{
|
||||
"Name": "Empty Placeholder",
|
||||
"Bookmark": "command {}"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(9, result.Data.Count);
|
||||
|
||||
// Should be identified as placeholders
|
||||
Assert.IsTrue(result.Data[0].IsPlaceholder, "Simple placeholder should be identified");
|
||||
Assert.IsTrue(result.Data[1].IsPlaceholder, "Multiple placeholders should be identified");
|
||||
Assert.IsTrue(result.Data[2].IsPlaceholder, "Web URL with placeholder should be identified");
|
||||
Assert.IsTrue(result.Data[3].IsPlaceholder, "Complex placeholder should be identified");
|
||||
Assert.IsTrue(result.Data[8].IsPlaceholder, "Empty placeholder should be identified");
|
||||
|
||||
// Should NOT be identified as placeholders
|
||||
Assert.IsFalse(result.Data[4].IsPlaceholder, "Regular URL should not be placeholder");
|
||||
Assert.IsFalse(result.Data[5].IsPlaceholder, "Local file should not be placeholder");
|
||||
Assert.IsFalse(result.Data[6].IsPlaceholder, "Only opening brace should not be placeholder");
|
||||
Assert.IsFalse(result.Data[7].IsPlaceholder, "Only closing brace should not be placeholder");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_MixedProperties_CorrectlyIdentifiesBothWebUrlAndPlaceholder()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "Web URL with Placeholder",
|
||||
"Bookmark": "https://google.com/search?q={query}"
|
||||
},
|
||||
{
|
||||
"Name": "Web URL without Placeholder",
|
||||
"Bookmark": "https://github.com"
|
||||
},
|
||||
{
|
||||
"Name": "Local File with Placeholder",
|
||||
"Bookmark": "notepad {file}"
|
||||
},
|
||||
{
|
||||
"Name": "Local File without Placeholder",
|
||||
"Bookmark": "C:\\Windows\\notepad.exe"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(4, result.Data.Count);
|
||||
|
||||
// Web URL with placeholder
|
||||
Assert.IsTrue(result.Data[0].IsWebUrl(), "Web URL with placeholder should be identified as web URL");
|
||||
Assert.IsTrue(result.Data[0].IsPlaceholder, "Web URL with placeholder should be identified as placeholder");
|
||||
|
||||
// Web URL without placeholder
|
||||
Assert.IsTrue(result.Data[1].IsWebUrl(), "Web URL without placeholder should be identified as web URL");
|
||||
Assert.IsFalse(result.Data[1].IsPlaceholder, "Web URL without placeholder should not be identified as placeholder");
|
||||
|
||||
// Local file with placeholder
|
||||
Assert.IsFalse(result.Data[2].IsWebUrl(), "Local file with placeholder should not be identified as web URL");
|
||||
Assert.IsTrue(result.Data[2].IsPlaceholder, "Local file with placeholder should be identified as placeholder");
|
||||
|
||||
// Local file without placeholder
|
||||
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file without placeholder should not be identified as web URL");
|
||||
Assert.IsFalse(result.Data[3].IsPlaceholder, "Local file without placeholder should not be identified as placeholder");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_EdgeCaseUrls_CorrectlyIdentifiesWebUrls()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "FTP URL",
|
||||
"Bookmark": "ftp://files.example.com"
|
||||
},
|
||||
{
|
||||
"Name": "HTTPS with port",
|
||||
"Bookmark": "https://localhost:8080"
|
||||
},
|
||||
{
|
||||
"Name": "IP Address",
|
||||
"Bookmark": "http://192.168.1.1"
|
||||
},
|
||||
{
|
||||
"Name": "Subdomain",
|
||||
"Bookmark": "https://api.github.com"
|
||||
},
|
||||
{
|
||||
"Name": "Domain only",
|
||||
"Bookmark": "example.com"
|
||||
},
|
||||
{
|
||||
"Name": "Not a URL - no dots",
|
||||
"Bookmark": "localhost"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(6, result.Data.Count);
|
||||
|
||||
Assert.IsFalse(result.Data[0].IsWebUrl(), "FTP URL should not be identified as web URL");
|
||||
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTPS with port should be identified as web URL");
|
||||
Assert.IsTrue(result.Data[2].IsWebUrl(), "IP Address with HTTP should be identified as web URL");
|
||||
Assert.IsTrue(result.Data[3].IsWebUrl(), "Subdomain should be identified as web URL");
|
||||
|
||||
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
|
||||
// Assert.IsTrue(result.Data[4].IsWebUrl(), "Domain only should be identified as web URL");
|
||||
Assert.IsFalse(result.Data[5].IsWebUrl(), "Single word without dots should not be identified as web URL");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class BookmarksCommandProviderTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void ProviderHasCorrectId()
|
||||
{
|
||||
// Setup
|
||||
var mockDataSource = new MockBookmarkDataSource();
|
||||
var provider = new BookmarksCommandProvider(mockDataSource);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Bookmarks", provider.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderHasDisplayName()
|
||||
{
|
||||
// Setup
|
||||
var mockDataSource = new MockBookmarkDataSource();
|
||||
var provider = new BookmarksCommandProvider(mockDataSource);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(provider.DisplayName);
|
||||
Assert.IsTrue(provider.DisplayName.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderHasIcon()
|
||||
{
|
||||
// Setup
|
||||
var provider = new BookmarksCommandProvider();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(provider.Icon);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TopLevelCommandsNotEmpty()
|
||||
{
|
||||
// Setup
|
||||
var provider = new BookmarksCommandProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
Assert.IsTrue(commands.Length > 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderWithMockData_LoadsBookmarksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var jsonData = @"{
|
||||
""Data"": [
|
||||
{
|
||||
""Name"": ""Test Bookmark"",
|
||||
""Bookmark"": ""https://test.com""
|
||||
},
|
||||
{
|
||||
""Name"": ""Another Bookmark"",
|
||||
""Bookmark"": ""https://another.com""
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
var dataSource = new MockBookmarkDataSource(jsonData);
|
||||
var provider = new BookmarksCommandProvider(dataSource);
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
|
||||
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
|
||||
var testBookmark = commands.Where(c => c.Title.Contains("Test Bookmark")).FirstOrDefault();
|
||||
|
||||
// Should have three commands:Add + two custom bookmarks
|
||||
Assert.AreEqual(3, commands.Length);
|
||||
|
||||
Assert.IsNotNull(addCommand);
|
||||
Assert.IsNotNull(testBookmark);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderWithEmptyData_HasOnlyAddCommand()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new MockBookmarkDataSource(@"{ ""Data"": [] }");
|
||||
var provider = new BookmarksCommandProvider(dataSource);
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
|
||||
// Only have Add command
|
||||
Assert.AreEqual(1, commands.Length);
|
||||
|
||||
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
|
||||
Assert.IsNotNull(addCommand);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderWithInvalidData_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new MockBookmarkDataSource("invalid json");
|
||||
var provider = new BookmarksCommandProvider(dataSource);
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
|
||||
// Only have one command. Will ignore json parse error.
|
||||
Assert.AreEqual(1, commands.Length);
|
||||
|
||||
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
|
||||
Assert.IsNotNull(addCommand);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks.UnitTests</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
internal sealed class MockBookmarkDataSource : IBookmarkDataSource
|
||||
{
|
||||
private string _jsonData;
|
||||
|
||||
public MockBookmarkDataSource(string initialJsonData = "[]")
|
||||
{
|
||||
_jsonData = initialJsonData;
|
||||
}
|
||||
|
||||
public string GetBookmarkData()
|
||||
{
|
||||
return _jsonData;
|
||||
}
|
||||
|
||||
public void SaveBookmarkData(string jsonData)
|
||||
{
|
||||
_jsonData = jsonData;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user