mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-08 05:17:03 +01:00
Compare commits
54 Commits
niels9001/
...
leilzh/tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06c21166b9 | ||
|
|
40bfdbc7c0 | ||
|
|
12c84b19c9 | ||
|
|
759f5c02cb | ||
|
|
e0428eef1d | ||
|
|
3bc746d0ff | ||
|
|
75526b9580 | ||
|
|
ce4d8dc11e | ||
|
|
917da2e07e | ||
|
|
ea6115f892 | ||
|
|
8dbff245d6 | ||
|
|
fa741470bc | ||
|
|
446d8087a3 | ||
|
|
a0a8ce9f69 | ||
|
|
7b06fb3bdb | ||
|
|
6130d2ad39 | ||
|
|
8737de29af | ||
|
|
2f6876b85f | ||
|
|
8f93d0269f | ||
|
|
d2a4c96e12 | ||
|
|
409ae3d73a | ||
|
|
65b752b3ff | ||
|
|
501ff5690d | ||
|
|
6acb793184 | ||
|
|
efb48aa163 | ||
|
|
e8754e4cd6 | ||
|
|
c4c9277f3f | ||
|
|
67cd0f055c | ||
|
|
051c07885e | ||
|
|
38acf1a6bc | ||
|
|
cb70e1dbfd | ||
|
|
df750394db | ||
|
|
9bcba8cb8b | ||
|
|
00457b2933 | ||
|
|
2b0b80c63a | ||
|
|
d42df8392c | ||
|
|
c6033bc8e5 | ||
|
|
69c206e85e | ||
|
|
0afb91b71d | ||
|
|
f926a5ece9 | ||
|
|
b9e66cca26 | ||
|
|
c4dd213712 | ||
|
|
2808f65d7f | ||
|
|
0e61b28571 | ||
|
|
ce406892b3 | ||
|
|
f619334ff2 | ||
|
|
142f09459d | ||
|
|
f42115bc4f | ||
|
|
36764487c5 | ||
|
|
218cb473c1 | ||
|
|
f0b674763a | ||
|
|
cf0264e9b9 | ||
|
|
be7098c267 | ||
|
|
3f486ea3db |
38
.github/actions/spell-check/expect.txt
vendored
38
.github/actions/spell-check/expect.txt
vendored
@@ -25,6 +25,8 @@ ADMINS
|
||||
adml
|
||||
admx
|
||||
advancedpaste
|
||||
advancedpasteui
|
||||
advancedpasteuishortcut
|
||||
advfirewall
|
||||
AFeature
|
||||
affordances
|
||||
@@ -40,6 +42,7 @@ ALLINPUT
|
||||
Allman
|
||||
Allmodule
|
||||
ALLOWUNDO
|
||||
allpc
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
AModifier
|
||||
@@ -115,6 +118,7 @@ bigbar
|
||||
bigobj
|
||||
binlog
|
||||
binres
|
||||
binskim
|
||||
BITMAPFILEHEADER
|
||||
bitmapimage
|
||||
BITMAPINFO
|
||||
@@ -255,6 +259,7 @@ Corpor
|
||||
cotaskmem
|
||||
COULDNOT
|
||||
countof
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
cplusplus
|
||||
@@ -627,6 +632,7 @@ HKCU
|
||||
hkey
|
||||
HKLM
|
||||
HKM
|
||||
hkmng
|
||||
HKPD
|
||||
HKU
|
||||
HMD
|
||||
@@ -635,6 +641,7 @@ hmodule
|
||||
hmonitor
|
||||
homies
|
||||
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
||||
HOOKPROC
|
||||
HORZRES
|
||||
HORZSIZE
|
||||
Hostbackdropbrush
|
||||
@@ -643,7 +650,11 @@ Hostx
|
||||
hotfixes
|
||||
hotkeycontrol
|
||||
HOTKEYF
|
||||
hotkeylockmachine
|
||||
hotkeyreconnect
|
||||
hotkeys
|
||||
hotkeyswitch
|
||||
hotkeytoggleeasymouse
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
@@ -701,9 +712,12 @@ IMAGERESIZERCONTEXTMENU
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
imagetotext
|
||||
imagetotextshortcut
|
||||
imagingdevices
|
||||
ime
|
||||
imgflip
|
||||
inapp
|
||||
inbox
|
||||
INCONTACT
|
||||
Indo
|
||||
@@ -757,6 +771,7 @@ istep
|
||||
ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
IUWP
|
||||
IWIC
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
@@ -786,6 +801,7 @@ keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
kvp
|
||||
Kybd
|
||||
lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
@@ -824,6 +840,7 @@ localappdata
|
||||
localpackage
|
||||
LOCALSYSTEM
|
||||
LOCATIONCHANGE
|
||||
LOCKMACHINE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
@@ -909,6 +926,7 @@ MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
measuretool
|
||||
meme
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
@@ -958,6 +976,7 @@ MOUSEHWHEEL
|
||||
MOUSEINPUT
|
||||
mousejump
|
||||
mousepointer
|
||||
mousepointercrosshairs
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
@@ -969,6 +988,7 @@ msc
|
||||
mscorlib
|
||||
msctls
|
||||
msdata
|
||||
msdia
|
||||
MSDL
|
||||
MSGFLT
|
||||
MSHCTX
|
||||
@@ -1157,6 +1177,18 @@ PARENTRELATIVEFORADDRESSBAR
|
||||
PARENTRELATIVEPARSING
|
||||
parray
|
||||
PARTIALCONFIRMATIONDIALOGTITLE
|
||||
pasteashtmlfile
|
||||
pasteashtmlfileshortcut
|
||||
pasteasjson
|
||||
pasteasjsonshortcut
|
||||
pasteasmarkdown
|
||||
pasteasmarkdownshortcut
|
||||
pasteasplaintext
|
||||
pasteasplaintextshortcut
|
||||
pasteaspngfile
|
||||
pasteaspngfileshortcut
|
||||
pasteastxtfile
|
||||
pasteastxtfileshortcut
|
||||
PATCOPY
|
||||
PATHMUSTEXIST
|
||||
PATINVERT
|
||||
@@ -1224,6 +1256,7 @@ Pomodoro
|
||||
Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
powerocr
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1364,6 +1397,7 @@ Removelnk
|
||||
renamable
|
||||
RENAMEONCOLLISION
|
||||
reparented
|
||||
reparenthotkey
|
||||
reparenting
|
||||
reportfileaccesses
|
||||
requery
|
||||
@@ -1613,6 +1647,7 @@ STYLECHANGED
|
||||
STYLECHANGING
|
||||
subkeys
|
||||
sublang
|
||||
Subdomain
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
@@ -1683,6 +1718,7 @@ THH
|
||||
THICKFRAME
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
thumbnailhotkey
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
timedate
|
||||
@@ -1697,6 +1733,7 @@ tlb
|
||||
tlbimp
|
||||
tlc
|
||||
TNP
|
||||
TOGGLEEASYMOUSE
|
||||
Toolhelp
|
||||
toolkitconverters
|
||||
toolwindow
|
||||
@@ -1710,6 +1747,7 @@ tracelogging
|
||||
tracerpt
|
||||
trackbar
|
||||
trafficmanager
|
||||
transcodetomp
|
||||
transicc
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -147,10 +147,10 @@ jobs:
|
||||
& "$(build.sourcesdirectory)\.pipelines\installPowerToys.ps1" -InstallMode "Machine"
|
||||
displayName: Install PowerToys (Machine-Level)
|
||||
|
||||
- ${{ if ne(parameters.platform, 'arm64') }}:
|
||||
- task: ScreenResolutionUtility@1
|
||||
inputs:
|
||||
displaySettings: 'optimal'
|
||||
# - ${{ if ne(parameters.platform, 'arm64') }}:
|
||||
# - task: ScreenResolutionUtility@1
|
||||
# inputs:
|
||||
# displaySettings: 'optimal'
|
||||
|
||||
- ${{ if eq(length(parameters.uiTestModules), 0) }}:
|
||||
- task: VSTest@3
|
||||
|
||||
@@ -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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1-76DF-42AC-985C-164CC2EE57A1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -2850,6 +2858,22 @@ Global
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -3161,6 +3185,8 @@ Global
|
||||
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -18,8 +18,8 @@ You can build the entire solution from the command line, which is sometimes fast
|
||||
1. Open Developer Command Prompt for VS 2022
|
||||
2. Navigate to the repository root directory
|
||||
3. Run the following command(don't forget to set the correct platform):
|
||||
```
|
||||
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln
|
||||
```pwsh
|
||||
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln /tl /p:NuGetInteractive="true"
|
||||
```
|
||||
4. This process should complete in approximately 13-14 minutes for a full build
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
Workspaces,
|
||||
PowerRename,
|
||||
CommandPalette,
|
||||
ScreenRuler,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -104,6 +105,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
[PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor"),
|
||||
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
|
||||
[PowerToysModule.CommandPalette] = new ModuleInfo("Microsoft.CmdPal.UI.exe", "PowerToys Command Palette", "WinUI3Apps\\CmdPal"),
|
||||
[PowerToysModule.ScreenRuler] = new ModuleInfo("PowerToys.MeasureToolUI.exe", "PowerToys.ScreenRuler", "WinUI3Apps"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,11 @@ struct CommonState
|
||||
|
||||
Measurement::Unit units = Measurement::Unit::Pixel;
|
||||
|
||||
POINT cursorPosSystemSpace = {}; // updated atomically
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4324)
|
||||
alignas(8) POINT cursorPosSystemSpace = {}; // updated atomically
|
||||
#pragma warning(pop)
|
||||
|
||||
std::atomic_bool closeOnOtherMonitors = false;
|
||||
|
||||
float GetPhysicalPx2MmRatio(HWND window) const
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="PowerToys.ScreenRuler"
|
||||
IsAlwaysOnTop="True"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
@@ -250,6 +251,7 @@
|
||||
<ToggleButton
|
||||
Name="btnBounds"
|
||||
x:Uid="BtnBounds"
|
||||
AutomationProperties.AutomationId="Button_Bounds"
|
||||
Click="BoundsTool_Click"
|
||||
Content=""
|
||||
KeyboardAcceleratorPlacementMode="Auto"
|
||||
@@ -267,6 +269,7 @@
|
||||
<ToggleButton
|
||||
Name="btnSpacing"
|
||||
x:Uid="BtnSpacing"
|
||||
AutomationProperties.AutomationId="Button_Spacing"
|
||||
Click="MeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -284,6 +287,7 @@
|
||||
<ToggleButton
|
||||
Name="btnHorizontalSpacing"
|
||||
x:Uid="BtnHorizontalSpacing"
|
||||
AutomationProperties.AutomationId="Button_SpacingHorizontal"
|
||||
Click="HorizontalMeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -304,6 +308,7 @@
|
||||
<ToggleButton
|
||||
Name="btnVerticalSpacing"
|
||||
x:Uid="BtnVerticalSpacing"
|
||||
AutomationProperties.AutomationId="Button_SpacingVertical"
|
||||
Click="VerticalMeasureTool_Click"
|
||||
Style="{StaticResource ToggleButtonRadioButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -324,6 +329,7 @@
|
||||
<AppBarSeparator />
|
||||
<Button
|
||||
x:Uid="BtnClosePanel"
|
||||
AutomationProperties.AutomationId="Button_Close"
|
||||
Click="ClosePanelTool_Click"
|
||||
Content=""
|
||||
Foreground="{StaticResource CloseButtonBackgroundPointerOver}">
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="d4d0f157-5c12-4390-9689-152b0c86a582"
|
||||
Publisher="CN=gkhmyznikov"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="d4d0f157-5c12-4390-9689-152b0c86a582" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>ScreenRuler.UITests</DisplayName>
|
||||
<PublisherDisplayName>gkhmyznikov</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="ScreenRuler.UITests"
|
||||
Description="ScreenRuler.UITests"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>PowerToys.ScreenRuler.UITests</RootNamespace>
|
||||
<AssemblyName>ScreenRuler.UITests</AssemblyName>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
|
||||
<!-- This is a UI test, so don't run as part of MSBuild -->
|
||||
<RunVSTest>false</RunVSTest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\ScreenRuler.UITests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestBounds : UITestBase
|
||||
{
|
||||
public TestBounds()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.BoundsTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerBoundsTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "bounds test");
|
||||
TestHelper.PerformBoundsToolTest(this);
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
466
src/modules/MeasureTool/Tests/ScreenRuler.UITests/TestHelper.cs
Normal file
466
src/modules/MeasureTool/Tests/ScreenRuler.UITests/TestHelper.cs
Normal file
@@ -0,0 +1,466 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
public static class TestHelper
|
||||
{
|
||||
private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
|
||||
|
||||
// Button automation names from Resources.resw
|
||||
public const string BoundsButtonId = "Button_Bounds";
|
||||
public const string SpacingButtonName = "Button_Spacing";
|
||||
public const string HorizontalSpacingButtonName = "Button_SpacingHorizontal";
|
||||
public const string VerticalSpacingButtonName = "Button_SpacingVertical";
|
||||
public const string CloseButtonId = "Button_Close";
|
||||
|
||||
/// <summary>
|
||||
/// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
/// <param name="testName">Name of the test for assertions</param>
|
||||
/// <returns>The activation keys for the test</returns>
|
||||
public static Key[] InitializeTest(UITestBase testBase, string testName)
|
||||
{
|
||||
LaunchFromSetting(testBase);
|
||||
|
||||
var toggleSwitch = SetScreenRulerToggle(testBase, enable: true);
|
||||
Assert.IsTrue(
|
||||
toggleSwitch.IsOn,
|
||||
$"Screen Ruler toggle switch should be ON for {testName}");
|
||||
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
Assert.IsNotNull(activationKeys, "Should be able to read activation shortcut");
|
||||
Assert.IsTrue(activationKeys.Length > 0, "Activation shortcut should contain at least one key");
|
||||
|
||||
return activationKeys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs common test cleanup: close ScreenRuler UI
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
public static void CleanupTest(UITestBase testBase)
|
||||
{
|
||||
CloseScreenRulerUI(testBase);
|
||||
|
||||
// Ensure we're attached to settings after cleanup
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors - this is just cleanup
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the Screen Ruler (Measure Tool) settings page
|
||||
/// </summary>
|
||||
public static void LaunchFromSetting(UITestBase testBase)
|
||||
{
|
||||
var screenRulers = testBase.Session.FindAll<NavigationViewItem>(By.AccessibilityId("Shell_Nav_ScreenRuler"));
|
||||
|
||||
if (screenRulers.Count == 0)
|
||||
{
|
||||
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("Shell_Nav_TopLevelSystemTools"), 5000).Click(msPostAction: 500);
|
||||
}
|
||||
|
||||
testBase.Session.Find<NavigationViewItem>(By.AccessibilityId("Shell_Nav_ScreenRuler"), 5000).Click(msPostAction: 500);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Screen Ruler toggle switch to the specified state
|
||||
/// </summary>
|
||||
public static ToggleSwitch SetScreenRulerToggle(UITestBase testBase, bool enable)
|
||||
{
|
||||
var toggleSwitch = testBase.Session.Find<ToggleSwitch>(By.AccessibilityId("Toggle_ScreenRuler"), 5000);
|
||||
|
||||
if (toggleSwitch.IsOn != enable)
|
||||
{
|
||||
toggleSwitch.Click(msPreAction: 1000, msPostAction: 2000);
|
||||
}
|
||||
|
||||
if (toggleSwitch.IsOn != enable)
|
||||
{
|
||||
testBase.Session.SendKey(Key.Space, msPreAction: 0, msPostAction: 2000);
|
||||
}
|
||||
|
||||
return toggleSwitch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Screen Ruler toggle and verify its state
|
||||
/// </summary>
|
||||
/// <param name="testBase">The test base instance</param>
|
||||
/// <param name="enable">True to enable, false to disable</param>
|
||||
/// <param name="testName">Name of the test for assertion messages</param>
|
||||
public static void SetAndVerifyScreenRulerToggle(UITestBase testBase, bool enable, string testName)
|
||||
{
|
||||
var toggleSwitch = SetScreenRulerToggle(testBase, enable);
|
||||
Assert.AreEqual(
|
||||
enable,
|
||||
toggleSwitch.IsOn,
|
||||
$"Screen Ruler toggle switch should be {(enable ? "ON" : "OFF")} for {testName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the current activation shortcut from the ShortcutControl
|
||||
/// </summary>
|
||||
public static Key[] ReadActivationShortcut(UITestBase testBase)
|
||||
{
|
||||
var shortcutCard = testBase.Session.Find<Element>(By.AccessibilityId("Shortcut_ScreenRuler"), 5000);
|
||||
var shortcutButton = shortcutCard.Find<Element>(By.AccessibilityId("EditButton"), 5000);
|
||||
return ParseShortcutText(shortcutButton.HelpText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse shortcut text like "Win + Ctrl + Shift + M" into Key array
|
||||
/// </summary>
|
||||
private static Key[] ParseShortcutText(string shortcutText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(shortcutText))
|
||||
{
|
||||
return new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.M };
|
||||
}
|
||||
|
||||
var keys = new List<Key>();
|
||||
var parts = shortcutText.Split(ShortcutSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var cleanPart = part.Trim().ToLowerInvariant();
|
||||
var key = cleanPart switch
|
||||
{
|
||||
"win" or "windows" => Key.Win,
|
||||
"ctrl" or "control" => Key.Ctrl,
|
||||
"shift" => Key.Shift,
|
||||
"alt" => Key.Alt,
|
||||
_ when cleanPart.Length == 1 && char.IsLetter(cleanPart[0]) &&
|
||||
cleanPart[0] >= 'a' && cleanPart[0] <= 'z' =>
|
||||
(Key)Enum.Parse(typeof(Key), cleanPart.ToUpperInvariant()),
|
||||
_ => (Key?)null,
|
||||
};
|
||||
|
||||
if (key.HasValue)
|
||||
{
|
||||
keys.Add(key.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return keys.Count > 0 ? keys.ToArray() : new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.M };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if ScreenRulerUI window is open
|
||||
/// </summary>
|
||||
public static bool IsScreenRulerUIOpen(UITestBase testBase) => testBase.IsWindowOpen("PowerToys.ScreenRuler");
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to reach the specified state within the timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUIState(UITestBase testBase, bool shouldBeOpen, int timeoutMs = 5000, int pollingIntervalMs = 100)
|
||||
{
|
||||
var endTime = DateTime.Now.AddMilliseconds(timeoutMs);
|
||||
|
||||
while (DateTime.Now < endTime)
|
||||
{
|
||||
if (IsScreenRulerUIOpen(testBase) == shouldBeOpen)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Task.Delay(pollingIntervalMs).Wait();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to appear within the specified timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUI(UITestBase testBase, int timeoutMs = 5000) =>
|
||||
WaitForScreenRulerUIState(testBase, shouldBeOpen: true, timeoutMs);
|
||||
|
||||
/// <summary>
|
||||
/// Wait for ScreenRulerUI to disappear within the specified timeout
|
||||
/// </summary>
|
||||
public static bool WaitForScreenRulerUIToDisappear(UITestBase testBase, int timeoutMs = 5000) =>
|
||||
WaitForScreenRulerUIState(testBase, shouldBeOpen: false, timeoutMs);
|
||||
|
||||
/// <summary>
|
||||
/// Close ScreenRulerUI if it's open
|
||||
/// </summary>
|
||||
public static void CloseScreenRulerUI(UITestBase testBase)
|
||||
{
|
||||
if (IsScreenRulerUIOpen(testBase))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Attach to ScreenRuler window before trying to find and click close button
|
||||
testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var closeButton = testBase.Session.Find<Element>(By.AccessibilityId(CloseButtonId), 15000, true);
|
||||
closeButton?.Click();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't find the close button, ignore - the window might have closed already
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Attach back to settings after closing
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific ScreenRulerUI button by its automation name
|
||||
/// </summary>
|
||||
public static Element? GetScreenRulerButton(UITestBase testBase, string buttonName, int timeoutMs = 1000)
|
||||
{
|
||||
return testBase.Session.Find<Element>(By.AccessibilityId(buttonName), timeoutMs, true);
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
// Attach to ScreenRuler window before trying to find buttons
|
||||
testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
return testBase.Session.Find<Element>(By.AccessibilityId(buttonName), timeoutMs, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Attach back to settings if needed for further operations
|
||||
// This ensures we don't break the test flow
|
||||
try
|
||||
{
|
||||
testBase.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore attachment errors - the calling code will handle as needed
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the clipboard content using STA thread
|
||||
/// </summary>
|
||||
public static void ClearClipboard()
|
||||
{
|
||||
ExecuteInSTAThread(() => System.Windows.Forms.Clipboard.Clear());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get text content from clipboard using STA thread
|
||||
/// </summary>
|
||||
public static string GetClipboardText()
|
||||
{
|
||||
string result = string.Empty;
|
||||
ExecuteInSTAThread(() =>
|
||||
{
|
||||
if (System.Windows.Forms.Clipboard.ContainsText())
|
||||
{
|
||||
result = System.Windows.Forms.Clipboard.GetText();
|
||||
}
|
||||
});
|
||||
return result ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute an action in an STA thread with error handling
|
||||
/// </summary>
|
||||
private static void ExecuteInSTAThread(Action action)
|
||||
{
|
||||
try
|
||||
{
|
||||
var staThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore clipboard errors
|
||||
}
|
||||
});
|
||||
|
||||
staThread.SetApartmentState(ApartmentState.STA);
|
||||
staThread.Start();
|
||||
staThread.Join(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore clipboard errors
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate clipboard content contains valid spacing measurement for the specified type
|
||||
/// </summary>
|
||||
public static bool ValidateSpacingClipboardContent(string clipboardText, string spacingType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipboardText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return spacingType switch
|
||||
{
|
||||
"Spacing" => Regex.IsMatch(clipboardText, @"\d+\s*[<5B>x]\s*\d+"),
|
||||
"Horizontal Spacing" or "Vertical Spacing" => Regex.IsMatch(clipboardText, @"^\d+$"),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a complete spacing tool test operation
|
||||
/// </summary>
|
||||
public static void PerformSpacingToolTest(UITestBase testBase, string buttonId, string testName)
|
||||
{
|
||||
ClearClipboard();
|
||||
|
||||
// Launch ScreenRuler UI
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
testBase.SendKeys(activationKeys);
|
||||
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUI(testBase, 2000),
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut for {testName}: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Attach to ScreenRuler window and click spacing button
|
||||
// testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var spacingButton = testBase.Session.Find<Element>(By.AccessibilityId(buttonId), 15000, true);
|
||||
Assert.IsNotNull(spacingButton, $"{testName} button should be found");
|
||||
|
||||
spacingButton!.Click();
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Perform measurement action (stay attached to ScreenRuler for this)
|
||||
PerformMeasurementAction(testBase);
|
||||
|
||||
// Validate results
|
||||
ValidateClipboardResults(testName);
|
||||
|
||||
// Cleanup - this will handle session attachment properly
|
||||
CloseScreenRulerUI(testBase);
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUIToDisappear(testBase, 2000),
|
||||
$"{testName}: ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a bounds tool test operation
|
||||
/// </summary>
|
||||
public static void PerformBoundsToolTest(UITestBase testBase)
|
||||
{
|
||||
ClearClipboard();
|
||||
|
||||
var activationKeys = ReadActivationShortcut(testBase);
|
||||
testBase.SendKeys(activationKeys);
|
||||
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUI(testBase, 2000),
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Attach to ScreenRuler window and click bounds button
|
||||
// testBase.Session.Attach(PowerToysModule.ScreenRuler);
|
||||
var boundsButton = testBase.Session.Find<Element>(By.AccessibilityId(BoundsButtonId), 15000, true);
|
||||
Assert.IsNotNull(boundsButton, "Bounds button should be found");
|
||||
|
||||
boundsButton.Click();
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Perform drag operation to create 100x100 box (stay attached to ScreenRuler)
|
||||
var currentPos = testBase.GetMousePosition();
|
||||
int startX = currentPos.Item1;
|
||||
int startY = currentPos.Item2 + 200;
|
||||
|
||||
testBase.MoveMouseTo(startX, startY);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
// Drag operation
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftDown);
|
||||
Task.Delay(100).Wait();
|
||||
|
||||
testBase.MoveMouseTo(startX + 99, startY + 99);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftUp);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Dismiss selection
|
||||
testBase.Session.PerformMouseAction(MouseActionType.RightClick);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
// Validate results
|
||||
string clipboardText = GetClipboardText();
|
||||
Assert.IsFalse(string.IsNullOrEmpty(clipboardText), "Clipboard should contain measurement data");
|
||||
Assert.IsTrue(
|
||||
clipboardText.Contains("100 <20> 100"),
|
||||
$"Clipboard should contain '100 <20> 100', but contained: '{clipboardText}'");
|
||||
|
||||
// Cleanup - this will handle session attachment properly
|
||||
CloseScreenRulerUI(testBase);
|
||||
Assert.IsTrue(
|
||||
WaitForScreenRulerUIToDisappear(testBase, 2000),
|
||||
"ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a measurement action (move mouse and click)
|
||||
/// </summary>
|
||||
private static void PerformMeasurementAction(UITestBase testBase)
|
||||
{
|
||||
var currentPos = testBase.GetMousePosition();
|
||||
int startX = currentPos.Item1;
|
||||
int startY = currentPos.Item2 + 200;
|
||||
|
||||
testBase.MoveMouseTo(startX, startY);
|
||||
Task.Delay(200).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.LeftClick);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
testBase.Session.PerformMouseAction(MouseActionType.RightClick);
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate clipboard results for spacing tests
|
||||
/// </summary>
|
||||
private static void ValidateClipboardResults(string testName)
|
||||
{
|
||||
string clipboardText = GetClipboardText();
|
||||
Assert.IsFalse(string.IsNullOrEmpty(clipboardText), $"{testName}: Clipboard should contain measurement data");
|
||||
|
||||
bool containsValidPattern = ValidateSpacingClipboardContent(clipboardText, testName);
|
||||
Assert.IsTrue(
|
||||
containsValidPattern,
|
||||
$"{testName}: Clipboard should contain valid spacing measurement, but contained: '{clipboardText}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestShortcutActivation : UITestBase
|
||||
{
|
||||
public TestShortcutActivation()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.ShortcutActivation")]
|
||||
[TestCategory("Activation")]
|
||||
public void TestScreenRulerShortcutActivation()
|
||||
{
|
||||
var activationKeys = TestHelper.InitializeTest(this, "activation test");
|
||||
|
||||
// Test 1: Press the activation shortcut and verify the toolbar appears
|
||||
SendKeys(activationKeys);
|
||||
bool screenRulerAppeared = TestHelper.WaitForScreenRulerUI(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerAppeared,
|
||||
$"ScreenRulerUI should appear after pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 2: Press the activation shortcut again and verify the toolbar disappears
|
||||
SendKeys(activationKeys);
|
||||
bool screenRulerDisappeared = TestHelper.WaitForScreenRulerUIToDisappear(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerDisappeared,
|
||||
$"ScreenRulerUI should disappear after pressing activation shortcut again: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 3: Disable Screen Ruler and verify that the activation shortcut no longer activates the utility
|
||||
// Ensure we're attached to settings UI before toggling
|
||||
Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
TestHelper.SetAndVerifyScreenRulerToggle(this, enable: false, "disabled state test");
|
||||
|
||||
// Try to activate with shortcut while disabled
|
||||
SendKeys(activationKeys);
|
||||
Task.Delay(1000).Wait();
|
||||
Assert.IsFalse(
|
||||
TestHelper.IsScreenRulerUIOpen(this),
|
||||
"ScreenRulerUI should not appear when Screen Ruler is disabled");
|
||||
|
||||
// Test 4: Enable Screen Ruler and press the activation shortcut and verify the toolbar appears
|
||||
// Ensure we're attached to settings UI before toggling
|
||||
Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
TestHelper.SetAndVerifyScreenRulerToggle(this, enable: true, "re-enabled state test");
|
||||
|
||||
SendKeys(activationKeys);
|
||||
screenRulerAppeared = TestHelper.WaitForScreenRulerUI(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerAppeared,
|
||||
$"ScreenRulerUI should appear after re-enabling and pressing activation shortcut: {string.Join(" + ", activationKeys)}");
|
||||
|
||||
// Test 5: Verify the utility can be closed via the cleanup method
|
||||
TestHelper.CloseScreenRulerUI(this);
|
||||
bool screenRulerClosed = TestHelper.WaitForScreenRulerUIToDisappear(this, 1000);
|
||||
Assert.IsTrue(
|
||||
screenRulerClosed,
|
||||
"ScreenRulerUI should close after calling CloseScreenRulerUI");
|
||||
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacing : UITestBase
|
||||
{
|
||||
public TestSpacing()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.SpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.SpacingButtonName, "Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacingHorizontal : UITestBase
|
||||
{
|
||||
public TestSpacingHorizontal()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.HorizontalSpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerHorizontalSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "horizontal spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.HorizontalSpacingButtonName, "Horizontal Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ScreenRuler.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class TestSpacingVertical : UITestBase
|
||||
{
|
||||
public TestSpacingVertical()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Large)
|
||||
{
|
||||
}
|
||||
|
||||
[TestMethod("ScreenRuler.VerticalSpacingTool")]
|
||||
[TestCategory("Spacing")]
|
||||
public void TestScreenRulerVerticalSpacingTool()
|
||||
{
|
||||
TestHelper.InitializeTest(this, "vertical spacing test");
|
||||
TestHelper.PerformSpacingToolTest(this, TestHelper.VerticalSpacingButtonName, "Vertical Spacing");
|
||||
TestHelper.CleanupTest(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="ScreenRuler.UITests.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -170,20 +170,29 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
// TODO: just added a lock around all of this anyway, but keeping the clone
|
||||
// while looking on some other ways to improve this; can be removed later.
|
||||
List<TopLevelViewModel> clone = [.. TopLevelCommands];
|
||||
var startIndex = -1;
|
||||
|
||||
var startIndex = FindIndexForFirstProviderItem(clone, sender.ProviderId);
|
||||
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
||||
clone.InsertRange(startIndex, newItems);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
static int FindIndexForFirstProviderItem(List<TopLevelViewModel> topLevelItems, string providerId)
|
||||
{
|
||||
// Tricky: all Commands from a single provider get added to the
|
||||
// top-level list all together, in a row. So if we find just the first
|
||||
// one, we can slice it out and insert the new ones there.
|
||||
for (var i = 0; i < clone.Count; i++)
|
||||
for (var i = 0; i < topLevelItems.Count; i++)
|
||||
{
|
||||
var wrapper = clone[i];
|
||||
var wrapper = topLevelItems[i];
|
||||
try
|
||||
{
|
||||
if (sender.ProviderId == wrapper.CommandProviderId)
|
||||
if (providerId == wrapper.CommandProviderId)
|
||||
{
|
||||
startIndex = i;
|
||||
break;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -191,9 +200,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
||||
clone.InsertRange(startIndex, newItems);
|
||||
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
||||
// If we didn't find any, then we just append the new commands to the end of the list.
|
||||
return topLevelItems.Count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +249,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
_extensionCommandProviders.Clear();
|
||||
}
|
||||
|
||||
if (extensions != null)
|
||||
if (extensions is not null)
|
||||
{
|
||||
await StartExtensionsAndGetCommands(extensions);
|
||||
}
|
||||
@@ -275,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)
|
||||
{
|
||||
@@ -285,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)
|
||||
{
|
||||
@@ -402,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 });
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened">
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
<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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -35,6 +36,17 @@ public partial class ContentIcon : FontIcon
|
||||
{
|
||||
if (this.FindDescendants().OfType<Grid>().FirstOrDefault() is Grid grid && Content is not null)
|
||||
{
|
||||
if (grid.Children.Contains(Content))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Content is FrameworkElement element && element.Parent is not null)
|
||||
{
|
||||
Debug.Assert(false, $"IconBoxElement Content is already parented to {element.Parent.GetType().Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
grid.Children.Add(Content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-16,-12,-12,-12"
|
||||
Fill="{ThemeResource MenuFlyoutSeparatorThemeBrush}" />
|
||||
Fill="{ThemeResource MenuFlyoutSeparatorBackground}" />
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user