Compare commits

...

61 Commits

Author SHA1 Message Date
Shawn Yuan
b22e53ca70 Resolve comments
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-05-12 17:04:22 +08:00
Shawn Yuan
10f4a9c2f0 Fixed comments.
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-05-12 10:00:42 +08:00
vanzue
e14609cbca Fix comments 2025-05-12 09:45:44 +08:00
vanzue
8700edc001 change level 2025-05-09 17:56:52 +08:00
vanzue
db08a0d517 change log level 2025-05-09 17:46:21 +08:00
vanzue
eb7b5a32d0 Merge branch 'shawn/addLogForCmdPalExt' of github.com:microsoft/PowerToys into shawn/addLogForCmdPalExt 2025-05-09 16:37:24 +08:00
Shawn Yuan
e70e6e5a71 Update
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-05-09 16:17:52 +08:00
vanzue
3d1b582826 remove noise 2025-05-09 16:16:01 +08:00
Shawn Yuan
9665eb533b Merge branch 'shawn/addLogForCmdPalExt' of https://github.com/microsoft/PowerToys into shawn/addLogForCmdPalExt 2025-05-09 15:35:56 +08:00
Shawn Yuan
d5cffb7458 Added logger for cmdpal extensions.
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-05-09 15:35:08 +08:00
vanzue
e74c88f853 fix 2025-05-09 14:33:57 +08:00
vanzue
ed505f0cb6 registry exception log 2025-05-09 14:28:30 +08:00
vanzue
ad90f4b944 Add bookmark log 2025-05-09 14:10:54 +08:00
vanzue
997af8e1c7 Add log to trace error for apps. 2025-05-09 14:07:12 +08:00
Shawn Yuan
cdc5f073f0 Fix settings crash issue when clicking "Open Cmdpal settings..." (#39322)
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-05-09 10:06:19 +08:00
Kayla Cinnamon
0427a7a7b0 Remove new label from command palette (#39318)
* remove new label from cmdpal

* fix xaml styling
2025-05-08 16:49:37 -07:00
Mike Griese
2d0d12f06c Bump toolkit version to 0.2.0 in template (#39292)
This bumps the version of the toolkit consumed by the template to 0.2.0

~Ironically, I have not yet published 0.2. I'm spinning that CI build currently. But I'll have that uploaded tomorrow morning at the latest~

EDIT: package is uploaded now
2025-05-07 16:52:26 -05:00
Mike Griese
06e5db6ff0 CmdPal: Actually observe Details (#39263)
Extensions can change the properties on their Details, and they should
be observable, but they weren't. This is because the ShellPage is
ultimately responsible for exposing the details, but it doesn't own the
details. The selected ListItemViewModel from the ListPage does.

This PR just adds a event handler on ListViewModel. We'll attach/detach
that handler to ListItemViewModels as the selection changes. In the body
of that handler, we'll let the ShellPage know when the details object
changes (by sending ShowDetails/HideDetails messages).

Closes #39216
2025-05-07 17:47:57 +08:00
Niels Laute
1a097ae09c [CmdPal] Open CmdPal settings from PT settings (#39262)
* Turning description into a hyperlink

* Update expect.txt

* Update CmdPalPage.xaml.cs

* update xaml format.

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update.

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>

* Added logger

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Co-authored-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-05-07 16:34:03 +08:00
Mike Griese
980ca46cdb CmdPal: URI activate, rather than using shell:AppsFolder (#39269)
* Use a URI handler for launching

this is a test

* I think we can review this

* Add a settings URI too
2025-05-07 11:30:47 +08:00
Kai Tao
6a1999d601 [cmdpal] protect cmdpal from crash if adaptive card fails (#39264)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? -->
## Summary of the Pull Request
     
Card = AdaptiveCard.FromJsonString(cardJson) is called in catch block, if it fails, app will crash.
2025-05-06 17:07:32 -05:00
Mike Griese
5655c61794 Lazy-fetch the settings for extensions (#38844)
This PR stops us from synchronously initializing the settings page for every extension (including built-in's) on startup. That incurs a small penalty that really adds up the more extensions a user has.

Instead, we'll now only initialize the `CommandSettings` object when we first actually need it. 

From a relatively unscientific test, this saves approximately 10% on the initialization of builtin commands, and for my setup, it trims about 28% off extension initialization (across all built-in's / extensions):

branch | Built-in load (ms) | Extension load (ms) | %Δ builtin | %Δ extensions | 
-- | -- | -- | -- | -- |
main | 1455 | 6867.6 | | |
this PR | 1309.2 | 4919 | -10.02% | -28.37%

Closes #38321
2025-05-06 04:58:44 -05:00
Mike Griese
371b7f0868 CmdPal: Cloak the window instead of hiding it (#39170)
This avoids the few frames of "flicker in" that XAML does when the window is SW_SHOW'n

closes #38384
closes #38404
closes #38438
2025-05-05 13:34:20 -05:00
Niels Laute
b6bcc92eb4 [CmdPal] MinWidth/Height and DPI-aware launch dimensions (#38637)
* MinWidth/Height and DPI-aware launch dimensions

* Making MainWindow DPI aware too

* Moving toastwindow to WinUIEx too

* Update MainWindow.xaml.cs

* Reverting back to the working logic

* Localizing settings window title

* Xaml formatting

* Update SettingsWindow.xaml.cs
2025-05-05 18:44:28 +02:00
Davide Giacometti
2ac464279a [CmdPal] Prevent maximizing (#39220)
Prevent CmdPal window from maximizing when user double-click the title bar area.

 Closes: #39096
2025-05-04 06:04:29 -05:00
Mike Griese
8ce198a47b cmdpal: fix a leak in the extension template (#39209)
There's apparently a footgun with the way we're using ComServer, which
results in us leaking the extension processes when we think we've
disposed them

The fix unfortunately has to be on the extension side. Extensions
published prior to 0.2 will need to manually fix this.

closes: #39045
2025-05-04 06:04:09 -05:00
Mike Griese
15ef9189ba cmdpal: unset the command if we don't find a command (#39208)
On a reload, the system commands fallback would leave the "restart"
fallback behind, for the same reason as what we found around
e40372c & ef264d9 in #38455
2025-05-04 06:03:01 -05:00
Mike Griese
2c555e2c2b Dismiss the details pane when the list gets emptied (#39206)
I cannot find an issue for this. I swear I filed it somewhere.

If you open winget, search for "terminal", wait till it loads, then
hit `esc`, we'll clear the search and empty the list, but never actually
hide the details pane. That looks weird.

This fixes that.

Closes _nothing i guess_.
2025-05-04 06:02:38 -05:00
Jerry Xu
83817700e1 [Infra-BuildScript] Add PowerShell Script to Enforce Shared Common.Dotnet.CsWinRT.props in all CSharp Projects under Src Sub-Folder (#37811)
* Add Powershell script to validate whether CSharp project correctly import shared props, update pipeline to enforce such validation, and fixed all projects that didn't import this shared props correctly

* add common props for fuzz test project

* update the path

* Only scans projects in src sub-folder

* Update .pipelines/verifyCommonProps.ps1

* Update csproj to include Common.Dotnet.CsWinRT.props

* Fix indentation in RegistryPreview.FuzzTests.csproj

* exclude TemplateCmdPalExtension.csproj in validation process

* exclude TemplateCmdPalExtension.csproj in validation process

---------

Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
Co-authored-by: Jerry Xu <nxu@microsoft.com>
2025-05-02 20:38:11 -07:00
Mike Griese
7e92a9a5e9 CmdPal: Start extensions in parallel (#39203)
This reduces our extension startup time by approximately 70% on my
machine (I have 17 extensions). I'd guess the gains scale with the
number of extensions. That's 8s -> 3s on average, and now I also get 2.5s reloads.

This retains the order of the list of extensions, by only starting the
processes in parallel. Once we have all the command provider instances,
then actually retrieving the commands.

It also adds a timeout on startup & load, so that one misbehaving extension won't block everyone else.

closes: #38529
2025-05-02 19:43:31 -05:00
Niels Laute
fe067def65 Fix for missing CmdPal extensions docs (#39173) 2025-05-01 18:33:01 +00:00
Yu Leng
6b9c99c2f6 [cmdpal][AOT] clean up some AOT related issue (#39163)
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-30 22:53:06 +08:00
Yu Leng
ba6af794ac [cmdpal] Support search any file in fallback command (#38455)
## Summary of the Pull Request
1. Add new setting to control this behaviour
2. Support always on and only when file exist in the fallback command
3. Change the condition of updateFallbackCommand in toplevelvm

demo:

https://github.com/user-attachments/assets/19e4ced3-30ad-44f4-8f3a-93620f46bb3d


## PR Checklist

- [x] **Closes:** #38370

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
2025-04-30 06:30:23 -05:00
Kai Tao
9cb99be4e9 [Tool] Delete export pfx function to remove use of hard coded password (#39144)
don't need export pfx functionality
2025-04-29 18:31:21 +08:00
Mike Griese
ad974bd679 Wait to update SearchText until we've actually updated SearchText (#39093)
Closes #38829

If we always UpdateProperty here, then there's a possible
race condition, where we raise the PropertyChanged(SearchText)
before the subclass actually retrieves the new SearchText from the
model. In that race situation, if the UI thread handles the
PropertyChanged before ListViewModel fetches the SearchText, it'll
think that the old search text is the _new_ value.
2025-04-27 15:29:15 -05:00
Yu Leng
49e5bbb5f0 [cmdpal][aot] Remove some unused file in CmdPal.Common and mark it as AOT compatible. (#39110)
* Remove unused com interface

* Remove unused file

* Remove unused file

* Remove unused file

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-27 17:18:01 +08:00
Kai Tao
7efbd2f013 [CmdPal] Launch cmd pal with retry (#39039)
* Add Retry when enable

* Add correct for the checking logic

* Retry in another thread (#39042)

* launch thread

* dev

* fix a thread safety

* improve

* improve

* make code clear

* Fix comment

* fix comment

* improve

* self review

* fix & log

* silent fail if not reach 10 times

* fix a ci build flag error

* fix a macro

* some simple improve

---------

Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
2025-04-27 13:47:56 +08:00
Clint Rutkas
ba230eca07 Start progress on AoT. (#39051)
* starting AoT flag push

* Few more

* bookmarks

* Really? The VM project compiles?

* Disable publish AOT before we really testing it.

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-27 13:44:47 +08:00
Gordon Lam
30df5e0df2 Update the ADO path for tsa.json (#39079)
The previous ADO area path for Powertoys was gone, we need to update to new one.
2025-04-27 08:59:44 +08:00
Yu Leng
9a6c64f9c0 [cmdpal] [AOT] make Clipboard/System/WebSearch/WindowsSettings ext become AOT compatible. (#39080)
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-25 16:22:50 +08:00
Corey Hayward
7dc2a05c45 [PTRun] Allow preventing usage based ordering results (#37491)
* Allow preventing selected result data retrieval

* Updated implementation to calculate sort order on result and update property name to better reflect purpose

* Update Result.cs sort order method name

Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>

* Align with the name GetSortOrderScore

---------

Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
2025-04-25 12:27:54 +08:00
Lemonyte
26fe36ab8d Color Picker: add Oklab and Oklch color formats (#38052)
* Resolve Resources.resw conflict

* Update CIE LCh chroma practical upper bound according to CSS spec

* Add review suggestions

* Add WIP tests (lch and oklch do not pass yet)

* Deduplicate Lab to LCh converter method

* Update expect.txt

* Fix liberty test color

* Reimplement oklab with better precision

* Remove CIE LCh

* Add tooltip for color param descriptions

* Update spell-check expect.txt with new words

* Remove 'cielch' and 'lch' from expect.txt

---------

Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
2025-04-25 10:48:19 +08:00
cryolithic
06b56a10bd 37405 Advanced Paste: Image To Text doesn't work with English (Canada) (#37806)
* [AdvancedPaste] [Fix Bug] Create ocrEngine from user profile language

GetOCRLanguage may fail based on language tag not matching (en-CA does not match en-GB or en-US), however user profile language may be valid.

* Update exception message.

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
2025-04-25 10:45:11 +08:00
Kai Tao
fc804a8156 [Tool] Script to build an installer locally (#39017)
* add script to build a installer

* minor fix

* fix search path for msix file

* fix sign

* fix sign

* fix spelling

* Fix powershell5 can't recognize emoji

* ensure-wix

* bring cmdpal available during local build

* remove early quit

* fix marco

* add logger

* doc

* add a note

* self review

* fix macro def

* add functionality to export cert so that other machine can install it.

* spelling
2025-04-25 09:57:42 +08:00
Mike Griese
f63fcfd91c Add support for filterable, nested context menus (#38776)
_targets #38573_

At first I just wanted to add support for nested context menus.

But then I also had to add a search box, so the focus wouldn't get weird.

End result:

![nested-menus-001](https://github.com/user-attachments/assets/4e8f1ec8-4b09-4095-9b81-caf7abde8aea)

This gets rid of the need to have the search box and the command bar both track item keybindings - now it's just in the command bar.

Closes #38299
Closes #38442
2025-04-24 13:32:07 -05:00
Davide Giacometti
195ff24a85 [Settings] Fix null CmdPal HotKey crash (#39052)
fixed null cmdpal hotkey crash when settings.json not exists or not define hotkey
2025-04-24 18:18:38 +08:00
Yu Leng
5691c5754b [cmdpal] Ref to AotCompatibility in some cmdpal project. (#39061)
* Ref to AotCompatibility

* Typo issue

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-24 16:07:10 +08:00
Shawn Yuan
100d560f9e [CmdPal] Added fallback for time and date (#38918)
* Added fallback for time and date

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* only support week/now/time/year query

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* Add week option

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* Changed setting for time date fallback.

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update globalization string

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* use week of year.

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
2025-04-24 14:58:01 +08:00
leileizhang
25c29ade8e upgrade the boost dependencies for Fuzzing Project (#39057) 2025-04-24 12:00:03 +08:00
Gordon Lam
8dfa55fe28 Update to WinAppSDK 1.7 latest version (#39016)
* Update to WinAppSDK 1.7 latest version
* Update UpdateVersions.ps1

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Co-authored-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
2025-04-24 09:25:47 +08:00
Gordon Lam
6c317c4ee1 Pre-creation of generated folder for any csproj (#39018)
* Test for Precreation of generated folder for any csproj to prevent random build break
2025-04-24 07:09:51 +08:00
Mike Griese
54e058e82d Fully initialize context menus when they change (#38998)
If we don't slow-initialize the whole menu when it changes, then we won't see that there's secondary (& more) commands.  

Tested this with the extension from [waaverecords/CmdPal.Ext.Spotify#4](https://github.com/waaverecords/CmdPal.Ext.Spotify/pull/4)

Closes #38959 


Also seemingly closes #38347 - seems that needed additional bumping of the `EmptyContent`'s
2025-04-23 10:54:01 -05:00
Mike Griese
b0e7473760 CmdPal: Tidy up some winget experiences (#38174)
I'm filing this so that I don't lose it on this machine I use less often. We can probably hold it out of 0.90


Fixes:
* If a package is installed, we always display the version as "Unknown"
  * also deals with a case where getting the package metadata could fail, and we'd hide the list item. That's only possible in the "installed, no updates available" case
* Allow package updates, add an icon for updates
* moves off the preview winget API onto a higher stable version
2025-04-23 04:45:34 -07:00
Niels Laute
f085ba0cd2 [CmdPal] Better support for long labels in empty content and commandbar (#38974)
Closes: #38970

Before:
<img width="605" alt="image" src="https://github.com/user-attachments/assets/8310e08f-c471-4663-9000-bfd1eb8c99f3" />

After:
<img width="514" alt="image" src="https://github.com/user-attachments/assets/b1e4c5f6-cd6b-42b2-9a23-3e1e3642202a" />
2025-04-23 04:40:33 -07:00
Niels Laute
884bfc71d3 [CmdPal] Tweaks to the detailspane UX (#38972)
Details pane UX tweaks. Closes: https://github.com/microsoft/PowerToys/issues/38973

Before:
![image](https://github.com/user-attachments/assets/ba7aca91-31ef-4571-b4ca-0951abe73c21)


After:
![image](https://github.com/user-attachments/assets/c17179b0-4319-4176-bac7-b4ca140bc624)
2025-04-23 04:35:19 -07:00
Davide Giacometti
252cf2670f [CmdPal] Hide commands that shouldn't be visible (#39011)
hide commands from more commands
2025-04-23 17:30:36 +08:00
leileizhang
4be6129835 [Fuzzing] Add PowerRename Fuzzing and C++ Project Setup Guidance (#38922)
* add fuzz

* update solution

* update pipeline

* update solution

* remove arm64

* use reference

* revert the code

* add fuzzing readme

* update solution

* fix spell-check

* Parent reference don't need update

* remove fuzzing config

* add debug config

* update the config
2025-04-23 17:05:29 +08:00
Kai Tao
583614449d [Workspaces]Fix for steam games capture&launch: capture and correctly launch steam games. (#38380)
* Workspaces fix: capture steam games.

* minor fix

* Launch steam apps by url appmodeluserid instead of directly exe call.

* fix copilot comment

* fix

* remove unnecessary string

* expect words

* white list words

* Order of alphabet

* exclude thin frame if it's not a steam game.

* fix build

* fix regression

* adjust comment
2025-04-23 15:39:54 +08:00
Yu Leng
d4e577bb81 [cmdpal] Add setting "ignore hotkey when full screen" (#38923)
* init

* merge main

* merge main

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-23 11:40:20 +08:00
Ionuț Manța
e1ad7e39c6 [PowerAccent] Cancel previous ShowToolbar task if a new one is triggered (#37757)
* Cancel previous ShowToolbar task if a new one is triggered

* more changes

* Fix space trigger starting at center

* Removed unneded code

* More changes

* Addressed feedback

* Fix another edge case
2025-04-23 11:03:05 +08:00
leileizhang
e8b02cd797 [ImageResizer]Fix: Deleting an Image Resizer preset deletes the wrong preset (#38476)
* [ImageResizer]Fix: Deleting an Image Resizer preset deletes the wrong preset

* update the helper

* sort items
2025-04-23 09:35:40 +08:00
223 changed files with 4093 additions and 1234 deletions

View File

@@ -273,4 +273,4 @@ mengyuanchen
testhost
#Tools
OIP
OIP

View File

@@ -8,6 +8,7 @@ Acceleratorkeys
ACCEPTFILES
ACCESSDENIED
ACCESSTOKEN
acfs
AClient
AColumn
acrt
@@ -197,6 +198,7 @@ CLIPBOARDUPDATE
CLIPCHILDREN
CLIPSIBLINGS
closesocket
clp
CLSCTX
clsids
Clusion
@@ -313,6 +315,7 @@ debugbreak
declatory
decryptor
Dedup
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER
DEFAULTCOLOR
DEFAULTFLAGS
@@ -518,11 +521,13 @@ FRAMECHANGED
frm
Froml
FROMTOUCH
fsanitize
fsmgmt
FZE
gacutil
Gaeilge
Gaidhlig
gameid
GC'ed
GCLP
gdi
@@ -629,6 +634,7 @@ HOTKEYF
hotkeys
hotlight
hotspot
Hostx
HPAINTBUFFER
HRAWINPUT
HREDRAW
@@ -712,6 +718,7 @@ INPUTSINK
INPUTTYPE
INSTALLDESKTOPSHORTCUT
INSTALLDIR
installdir
INSTALLFOLDER
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
@@ -1040,6 +1047,7 @@ NOINHERITLAYOUT
NOINTERFACE
NOINVERT
NOLINKINFO
nologo
NOMCX
NOMINMAX
NOMIRRORBITMAP
@@ -1077,6 +1085,7 @@ NOTRACK
NOTSRCCOPY
NOTSRCERASE
NOTXORPEN
notwindows
NOZORDER
NPH
npmjs
@@ -1271,6 +1280,7 @@ pstm
PStr
pstream
pstrm
pswd
PSYSTEM
psz
ptb
@@ -1398,6 +1408,7 @@ sacl
safeprojectname
SAMEKEYPREVIOUSLYMAPPED
SAMESHORTCUTPREVIOUSLYMAPPED
sancov
SAVEFAILED
scanled
schedtasks
@@ -1416,6 +1427,7 @@ searchterm
SEARCHUI
SECONDARYDISPLAY
secpol
securestring
SEEMASKINVOKEIDLIST
SELCHANGE
SENDCHANGE
@@ -1569,6 +1581,7 @@ stdcpp
stdcpplatest
STDMETHODCALLTYPE
STDMETHODIMP
steamapps
STGC
STGM
STGMEDIUM
@@ -1932,6 +1945,7 @@ WUX
Wwanpp
XAxis
xclip
xcopy
XDocument
XElement
xfd
@@ -1970,3 +1984,11 @@ ZOOMITX
ZXk
ZXNs
zzz
ACIE
AOklab
BCIE
BOklab
culori
Evercoder
LCh
CIELCh

View File

@@ -1,16 +1,24 @@
Param(
# Using the default value of 1.6 for winAppSdkVersionNumber and useExperimentalVersion as false
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
[Parameter(Mandatory=$False,Position=1)]
[string]$winAppSdkVersionNumber = "1.6",
[string]$winAppSdkVersionNumber = "1.7",
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
[Parameter(Mandatory=$False,Position=2)]
[boolean]$useExperimentalVersion = $False
[boolean]$useExperimentalVersion = $False,
# Root folder Path for processing
[Parameter(Mandatory=$False,Position=3)]
[string]$rootPath = $(Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)),
# Root folder Path for processing
[Parameter(Mandatory=$False,Position=4)]
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
)
function Update-NugetConfig {
param (
[string]$filePath = "nuget.config"
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
)
Write-Host "Updating nuget.config file"
@@ -35,7 +43,33 @@ function Update-NugetConfig {
$xml.Save($filePath)
}
$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
function Read-FileWithEncoding {
param (
[string]$Path
)
$reader = New-Object System.IO.StreamReader($Path, $true) # auto-detect encoding
$content = $reader.ReadToEnd()
$encoding = $reader.CurrentEncoding
$reader.Close()
return [PSCustomObject]@{
Content = $content
Encoding = $encoding
}
}
function Write-FileWithEncoding {
param (
[string]$Path,
[string]$Content,
[System.Text.Encoding]$Encoding
)
$writer = New-Object System.IO.StreamWriter($Path, $false, $Encoding)
$writer.Write($Content)
$writer.Close()
}
# Execute nuget list and capture the output
if ($useExperimentalVersion) {
@@ -79,50 +113,54 @@ if ($latestVersion) {
}
# Update packages.config files
Get-ChildItem -Recurse packages.config | ForEach-Object {
$content = Get-Content $_.FullName -Raw
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update Directory.Packages.props file
$propsFile = "Directory.Packages.props"
$propsFile = [System.IO.Path]::Combine($rootPath,"Directory.Packages.props")
if (Test-Path $propsFile) {
$content = Get-Content $propsFile -Raw
$file = Read-FileWithEncoding -Path $propsFile
$content = $file.Content
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $propsFile -Value $content
Write-FileWithEncoding -Path $propsFile -Content $content -Encoding $file.encoding
Write-Host "Modified " $propsFile
}
}
# Update .vcxproj files
Get-ChildItem -Recurse *.vcxproj | ForEach-Object {
$content = Get-Content $_.FullName -Raw
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match '\\Microsoft.WindowsAppSDK.') {
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion + '\'
$oldVersionString = '\\Microsoft.WindowsAppSDK.[-.0-9a-zA-Z]*\\'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update .csproj files
Get-ChildItem -Recurse *.csproj | ForEach-Object {
$content = Get-Content $_.FullName -Raw
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
$file = Read-FileWithEncoding -Path $_.FullName
$content = $file.Content
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}

View File

@@ -3,5 +3,5 @@
"notificationAliases": ["powertoys@microsoft.com"],
"instanceUrl": "https://microsoft.visualstudio.com",
"projectName": "OS",
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys"
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\DIVE\\PowerToys"
}

View File

@@ -33,7 +33,7 @@ parameters:
default: true
- name: winAppSDKVersionNumber
type: string
default: 1.6
default: 1.7
- name: useExperimentalVersion
type: boolean
default: false

View File

@@ -344,6 +344,11 @@ jobs:
flattenFolders: True
OverWrite: True
# Check if all projects (located in src sub-folder) import common props
- pwsh: |-
& '.pipelines/verifyCommonProps.ps1' -sourceDir '$(build.sourcesdirectory)\src'
displayName: Audit shared common props for CSharp projects in src sub-folder
# Check if deps.json files don't reference different dll versions.
- pwsh: |-
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)'

View File

@@ -17,6 +17,7 @@ steps:
arguments: >
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
-rootPath "$(build.sourcesdirectory)"
- script: echo $(WinAppSDKVersion)
displayName: 'Display WinAppSDK Version Found'

View File

@@ -0,0 +1,54 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True, Position = 1)]
[string]$sourceDir
)
# scan all csharp project in the source directory
function Get-CSharpProjects {
param (
[string]$path
)
# Get all .csproj files under the specified path
return Get-ChildItem -Path $path -Recurse -Filter *.csproj | Select-Object -ExpandProperty FullName
}
# Check if the project file imports 'Common.Dotnet.CsWinRT.props'
function Test-ImportSharedCsWinRTProps {
param (
[string]$filePath
)
# Load the XML content of the .csproj file
[xml]$csprojContent = Get-Content -Path $filePath
# Check if the Import element with Project attribute containing 'Common.Dotnet.CsWinRT.props' exists
return $csprojContent.Project.Import | Where-Object { $null -ne $_.Project -and $_.Project.EndsWith('Common.Dotnet.CsWinRT.props') }
}
# Call the function with the provided source directory
$csprojFilesArray = Get-CSharpProjects -path $sourceDir
$hasInvalidCsProj = $false
# Enumerate the array of file paths and call Validate-ImportSharedCsWinRTProps for each file
foreach ($csprojFile in $csprojFilesArray) {
# Skip if the file ends with 'TemplateCmdPalExtension.csproj'
if ($csprojFile -like '*TemplateCmdPalExtension.csproj') {
continue
}
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
if (!$importExists) {
Write-Output "$csprojFile need to import 'Common.Dotnet.CsWinRT.props'."
$hasInvalidCsProj = $true
}
}
if ($hasInvalidCsProj) {
exit 1
}
exit 0

View File

@@ -45,7 +45,7 @@
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.4" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.120-preview" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.4" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
@@ -55,7 +55,7 @@
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250401001" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />

View File

@@ -1472,8 +1472,8 @@ SOFTWARE.
- Microsoft.Windows.CsWin32 0.2.46-beta
- Microsoft.Windows.CsWinRT 2.2.0
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
- Microsoft.WindowsAppSDK 1.6.250205002
- Microsoft.WindowsPackageManager.ComInterop 1.10.120-preview
- Microsoft.WindowsAppSDK 1.7.250401001
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
- ModernWpfUI 0.9.4

View File

@@ -708,6 +708,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalKeyboardService", "src\modules\cmdpal\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj", "{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.FuzzingTest", "src\modules\powerrename\PowerRename.FuzzingTest\PowerRename.FuzzingTest.vcxproj", "{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2588,6 +2590,12 @@ Global
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.Build.0 = Release|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.ActiveCfg = Release|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.Build.0 = Release|x64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|ARM64.ActiveCfg = Debug|ARM64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.ActiveCfg = Debug|x64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.Build.0 = Debug|x64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|ARM64.ActiveCfg = Release|ARM64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.ActiveCfg = Release|x64
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2859,6 +2867,7 @@ Global
{5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
{64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -0,0 +1,117 @@
# 🧪 C++ Project Fuzzing Test Guide
This guide walks you through setting up a **fuzzing test** project for a C++ module using [libFuzzer](https://llvm.org/docs/LibFuzzer.html).
.
---
## 🏗️ Step-by-Step Setup
### 1. Create a New C++ Project
- Use **Empty Project** template.
- Name it `<ModuleName>.FuzzingTest`.
---
### 2. Update Build Configuration
- In **Configuration Manager**, Uncheck Build for both Release|ARM64, Debug|ARM64 and Debug|x64 configurations.
- Note: ARM64 is not supported in this case, so leave ARM64 configurations build disabled.
---
### 3. Enable ASan and libFuzzer in `.vcxproj`
Edit the project file to enable fuzzing:
```xml
<PropertyGroup>
<EnableASAN>true</EnableASAN>
<EnableFuzzer>true</EnableFuzzer>
</PropertyGroup>
```
---
### 4. Add Fuzzing Compiler Flags
Add this to `AdditionalOptions` under the `Fuzzing` configuration:
```xml
/fsanitize=address
/fsanitize-coverage=inline-8bit-counters
/fsanitize-coverage=edge
/fsanitize-coverage=trace-cmp
/fsanitize-coverage=trace-div
%(AdditionalOptions)
```
---
### 5. Link the Sanitizer Coverage Runtime
In `Linker → Input → Additional Dependencies`, add:
```text
$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib
```
---
### 6. Copy Required Runtime DLL
Add a `PostBuildEvent` to copy the ASAN DLL:
```xml
<Command>
xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
</Command>
```
---
### 7. Add Preprocessor Definitions
To avoid annotation issues, add these to the `Preprocessor Definitions`:
```text
_DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION
```
---
## 🧬 Required Code
### `LLVMFuzzerTestOneInput` Entry Point
Every fuzzing project must expose this function:
```cpp
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
std::string input(reinterpret_cast<const char*>(data), size);
try
{
// Call your module with the input here.
}
catch (...) {}
return 0;
}
```
---
## ⚙️ [Test run in the cloud](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/faq/notwindows/walkthrough)
To submit a job to the cloud you can run with this command:
```
oip submit --config .\OneFuzzConfig.json --drop-path <your_submission_directory> --platform windows --do-not-file-bugs --duration 1
```
You want to run with --do-not-file-bugs because if there is an issue with running the parser in the cloud (which is very possible), you don't want bugs to be created if there is an issue. The --duration task is the number of hours you want the task to run. I recommend just running for 1 hour to make sure things work initially. If you don't specify this parameter, it will default to 48 hours. You can find more about submitting a test job here.
OneFuzz will send you an email when the job has started.
---

View File

@@ -79,10 +79,7 @@
<ComponentGroupRef Id="ToolComponentGroup" />
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
<ComponentGroupRef Id="WorkspacesComponentGroup" />
<?if $(var.CIBuild) = "true" ?>
<ComponentGroupRef Id="CmdPalComponentGroup" />
<?endif?>
<ComponentGroupRef Id="CmdPalComponentGroup" />
</Feature>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />

View File

@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Some items may be set in Directory.Build.props in root -->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project=".\Common.Dotnet.PrepareGeneratedFolder.targets" />
<PropertyGroup>
<WindowsSdkPackageVersion>10.0.22621.57</WindowsSdkPackageVersion>
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Some items may be set in Directory.Build.props in root -->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- OneFuzz does not currently support testing with .NET 9.
As a temporary workaround, create a .NET 8 project and use file links
to include the code that needs testing. -->
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="EnsureGeneratedBaseFolder" BeforeTargets="XamlPreCompile">
<PropertyGroup>
<!-- Only create the base 'generated' folder -->
<CompilerGeneratedFilesOutputPath>$(ProjectDir)obj\g</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<!-- Create 'generated' folder if missing -->
<MakeDir Directories="$(CompilerGeneratedFilesOutputPath)" />
<!-- Optional logging for debugging -->
<Message Text="Ensured: $(GeneratedBasePath)" Importance="Low" />
</Target>
</Project>

View File

@@ -141,6 +141,40 @@ namespace ManagedCommon
return lab;
}
/// <summary>
/// Convert a given <see cref="Color"/> to a Oklab color
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
public static (double Lightness, double ChromaticityA, double ChromaticityB) ConvertToOklabColor(Color color)
{
var linear = ConvertSRGBToLinearRGB(color.R / 255d, color.G / 255d, color.B / 255d);
var oklab = GetOklabColorFromLinearRGB(linear.R, linear.G, linear.B);
return oklab;
}
/// <summary>
/// Convert a given <see cref="Color"/> to a Oklch color
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
public static (double Lightness, double Chroma, double Hue) ConvertToOklchColor(Color color)
{
var oklab = ConvertToOklabColor(color);
var oklch = GetOklchColorFromOklab(oklab.Lightness, oklab.ChromaticityA, oklab.ChromaticityB);
return oklch;
}
public static (double R, double G, double B) ConvertSRGBToLinearRGB(double r, double g, double b)
{
// inverse companding, gamma correction must be undone
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
return (rLinear, gLinear, bLinear);
}
/// <summary>
/// Convert a given <see cref="Color"/> to a CIE XYZ color (XYZ)
/// The constants of the formula matches this Wikipedia page, but at a higher precision:
@@ -156,10 +190,7 @@ namespace ManagedCommon
double g = color.G / 255d;
double b = color.B / 255d;
// inverse companding, gamma correction must be undone
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
(double rLinear, double gLinear, double bLinear) = ConvertSRGBToLinearRGB(r, g, b);
return (
(rLinear * 0.41239079926595948) + (gLinear * 0.35758433938387796) + (bLinear * 0.18048078840183429),
@@ -210,6 +241,63 @@ namespace ManagedCommon
return (l, a, b);
}
/// <summary>
/// Convert a linear RGB color <see cref="double"/> to an Oklab color.
/// The constants of this formula come from https://github.com/Evercoder/culori/blob/2bedb8f0507116e75f844a705d0b45cf279b15d0/src/oklab/convertLrgbToOklab.js
/// and the implementation is based on https://bottosson.github.io/posts/oklab/
/// </summary>
/// <param name="r">Linear R value</param>
/// <param name="g">Linear G value</param>
/// <param name="b">Linear B value</param>
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
private static (double Lightness, double ChromaticityA, double ChromaticityB)
GetOklabColorFromLinearRGB(double r, double g, double b)
{
double l = (0.41222147079999993 * r) + (0.5363325363 * g) + (0.0514459929 * b);
double m = (0.2119034981999999 * r) + (0.6806995450999999 * g) + (0.1073969566 * b);
double s = (0.08830246189999998 * r) + (0.2817188376 * g) + (0.6299787005000002 * b);
double l_ = Math.Cbrt(l);
double m_ = Math.Cbrt(m);
double s_ = Math.Cbrt(s);
return (
(0.2104542553 * l_) + (0.793617785 * m_) - (0.0040720468 * s_),
(1.9779984951 * l_) - (2.428592205 * m_) + (0.4505937099 * s_),
(0.0259040371 * l_) + (0.7827717662 * m_) - (0.808675766 * s_)
);
}
/// <summary>
/// Convert an Oklab color <see cref="double"/> from Cartesian form to its polar form Oklch
/// https://bottosson.github.io/posts/oklab/#the-oklab-color-space
/// </summary>
/// <param name="lightness">The <see cref="lightness"/></param>
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
private static (double Lightness, double Chroma, double Hue)
GetOklchColorFromOklab(double lightness, double chromaticity_a, double chromaticity_b)
{
return GetLCHColorFromLAB(lightness, chromaticity_a, chromaticity_b);
}
/// <summary>
/// Convert a color in Cartesian form (Lab) to its polar form (LCh)
/// </summary>
/// <param name="lightness">The <see cref="lightness"/></param>
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
/// <returns>The lightness, chroma, and hue angle</returns>
private static (double Lightness, double Chroma, double Hue)
GetLCHColorFromLAB(double lightness, double chromaticity_a, double chromaticity_b)
{
// Lab to LCh transformation
double chroma = Math.Sqrt(Math.Pow(chromaticity_a, 2) + Math.Pow(chromaticity_b, 2));
double hue = Math.Round(chroma, 3) == 0 ? 0.0 : ((Math.Atan2(chromaticity_b, chromaticity_a) * 180d / Math.PI) + 360d) % 360d;
return (lightness, chroma, hue);
}
/// <summary>
/// Convert a given <see cref="Color"/> to a natural color (hue, whiteness, blackness)
/// </summary>
@@ -276,12 +364,17 @@ namespace ManagedCommon
{ "Br", 'p' }, // brightness percent
{ "In", 'p' }, // intensity percent
{ "Ll", 'p' }, // lightness (HSL) percent
{ "Lc", 'p' }, // lightness(CIELAB)percent
{ "Va", 'p' }, // value percent
{ "Wh", 'p' }, // whiteness percent
{ "Bn", 'p' }, // blackness percent
{ "Ca", 'p' }, // chromaticityA percent
{ "Cb", 'p' }, // chromaticityB percent
{ "Lc", 'p' }, // lightness (CIE) percent
{ "Ca", 'p' }, // chromaticityA (CIELAB) percent
{ "Cb", 'p' }, // chromaticityB (CIELAB) percent
{ "Lo", 'p' }, // lightness (Oklab/Oklch) percent
{ "Oa", 'p' }, // chromaticityA (Oklab) percent
{ "Ob", 'p' }, // chromaticityB (Oklab) percent
{ "Oc", 'p' }, // chroma (Oklch) percent
{ "Oh", 'p' }, // hue angle (Oklch) percent
{ "Xv", 'i' }, // X value int
{ "Yv", 'i' }, // Y value int
{ "Zv", 'i' }, // Z value int
@@ -424,6 +517,10 @@ namespace ManagedCommon
var (lightnessC, _, _) = ConvertToCIELABColor(color);
lightnessC = Math.Round(lightnessC, 2);
return lightnessC.ToString(CultureInfo.InvariantCulture);
case "Lo":
var (lightnessO, _, _) = ConvertToOklabColor(color);
lightnessO = Math.Round(lightnessO, 2);
return lightnessO.ToString(CultureInfo.InvariantCulture);
case "Wh":
var (_, whiteness, _) = ConvertToHWBColor(color);
whiteness = Math.Round(whiteness * 100);
@@ -440,6 +537,22 @@ namespace ManagedCommon
var (_, _, chromaticityB) = ConvertToCIELABColor(color);
chromaticityB = Math.Round(chromaticityB, 2);
return chromaticityB.ToString(CultureInfo.InvariantCulture);
case "Oa":
var (_, chromaticityAOklab, _) = ConvertToOklabColor(color);
chromaticityAOklab = Math.Round(chromaticityAOklab, 2);
return chromaticityAOklab.ToString(CultureInfo.InvariantCulture);
case "Ob":
var (_, _, chromaticityBOklab) = ConvertToOklabColor(color);
chromaticityBOklab = Math.Round(chromaticityBOklab, 2);
return chromaticityBOklab.ToString(CultureInfo.InvariantCulture);
case "Oc":
var (_, chromaOklch, _) = ConvertToOklchColor(color);
chromaOklch = Math.Round(chromaOklch, 2);
return chromaOklch.ToString(CultureInfo.InvariantCulture);
case "Oh":
var (_, _, hueOklch) = ConvertToOklchColor(color);
hueOklch = Math.Round(hueOklch, 2);
return hueOklch.ToString(CultureInfo.InvariantCulture);
case "Xv":
var (x, _, _) = ConvertToCIEXYZColor(color);
x = Math.Round(x * 100, 4);
@@ -495,8 +608,10 @@ namespace ManagedCommon
case "HSI": return "hsi(%Hu, %Si%, %In%)";
case "HWB": return "hwb(%Hu, %Wh%, %Bn%)";
case "NCol": return "%Hn, %Wh%, %Bn%";
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
case "CIEXYZ": return "XYZ(%Xv, %Yv, %Zv)";
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
case "Oklab": return "oklab(%Lo, %Oa, %Ob)";
case "Oklch": return "oklch(%Lo, %Oc, %Oh)";
case "VEC4": return "(%Reff, %Grff, %Blff, 1f)";
case "Decimal": return "%Dv";
case "HEX Int": return "0xFF%ReX%GrX%BlX";

View File

@@ -0,0 +1,51 @@
// 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.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ManagedCommon
{
public static class IdRecoveryHelper
{
/// <summary>
/// Fixes invalid IDs in the given list by assigning unique values.
/// It ensures that all IDs are non-empty and unique, correcting any duplicates or empty IDs.
/// </summary>
/// <param name="items">The list of items that may contain invalid IDs.</param>
public static void RecoverInvalidIds<T>(IEnumerable<T> items)
where T : class, IHasId
{
var idSet = new HashSet<int>();
int newId = 0;
var sortedItems = items.OrderBy(i => i.Id).ToList(); // Sort items by ID for consistent processing
// Iterate through the list and fix invalid IDs
foreach (var item in sortedItems)
{
// If the ID is invalid or already exists in the set (duplicate), assign a new unique ID
if (!idSet.Add(item.Id))
{
// Find the next available unique ID
while (idSet.Contains(newId))
{
newId++;
}
item.Id = newId;
idSet.Add(newId); // Add the newly assigned ID to the set
}
}
}
}
public interface IHasId
{
int Id { get; set; }
}
}

View File

@@ -301,6 +301,7 @@ namespace package
if (!std::filesystem::exists(directoryPath))
{
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
return {};
}
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);

View File

@@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

View File

@@ -18,10 +18,19 @@ public static class OcrHelpers
{
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
{
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
var ocrLanguage = GetOCRLanguage();
cancellationToken.ThrowIfCancellationRequested();
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
OcrEngine ocrEngine;
if (ocrLanguage is not null)
{
ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine from specified language");
}
else
{
ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages() ?? throw new InvalidOperationException("Unable to create OCR engine from user profile language");
}
cancellationToken.ThrowIfCancellationRequested();
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);

View File

@@ -15,8 +15,6 @@
<ProjectPriFileName>PowerToys.EnvironmentVariablesUILib.pri</ProjectPriFileName>
<GenerateLibraryLayout>true</GenerateLibraryLayout>
<IsPackable>true</IsPackable>
<!-- The default generated file path exceeds the length limit 260 on the build agent. Using a shorter path as a workaround. -->
<CompilerGeneratedFilesOutputPath>$(ProjectDir)obj\g</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<PropertyGroup>
@@ -56,7 +54,4 @@
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<Target Name="EnsureCompilerGeneratedFilesOutputPathExists" BeforeTargets="XamlPreCompile">
<MakeDir Directories="$(CompilerGeneratedFilesOutputPath)" />
</Target>
</Project>

View File

@@ -1,6 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
@@ -141,7 +141,7 @@
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -153,7 +153,7 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets'))" />
</Target>
</Project>

View File

@@ -4,5 +4,5 @@
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.22621.2428" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.6.250205002" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.7.250401001" targetFramework="native" />
</packages>

View File

@@ -121,6 +121,22 @@ namespace AppLauncher
// packaged apps: try launching first by AppUserModel.ID
// usage example: elevated Terminal
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
{
Logger::trace(L"Launching {} as {} - {app.packageFullName}", app.name, app.appUserModelId, app.packageFullName);
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
if (res.isOk())
{
launched = true;
}
else
{
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
}
}
// win32 app with appUserModelId:
// usage example: steam games
if (!launched && !app.appUserModelId.empty())
{
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);

View File

@@ -1,5 +1,6 @@
#include "pch.h"
#include "AppUtils.h"
#include "SteamHelper.h"
#include <atlbase.h>
#include <propvarutil.h>
@@ -34,6 +35,8 @@ namespace Utils
constexpr const wchar_t* EdgeFilename = L"msedge.exe";
constexpr const wchar_t* ChromeFilename = L"chrome.exe";
constexpr const wchar_t* SteamUrlProtocol = L"steam:";
}
AppList IterateAppsFolder()
@@ -138,6 +141,34 @@ namespace Utils
else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp)
{
data.installPath = propVariantString.m_pData;
if (!data.installPath.empty())
{
const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
if (isSteamProtocol)
{
Logger::info(L"Found steam game: protocol path: {}", data.installPath);
data.protocolPath = data.installPath;
try
{
auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath);
auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId);
if (gameFolder)
{
data.installPath = gameFolder->gameInstallationPath;
Logger::info(L"Found steam game: physical path: {}", data.installPath);
}
}
catch (std::exception ex)
{
Logger::error(L"Failed to get installPath for game {}", data.installPath);
Logger::error("Error: {}", ex.what());
}
}
}
}
}
@@ -397,5 +428,10 @@ namespace Utils
{
return installPath.ends_with(NonLocalizable::ChromeFilename);
}
bool AppData::IsSteamGame() const
{
return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
}
}
}

View File

@@ -13,10 +13,12 @@ namespace Utils
std::wstring packageFullName;
std::wstring appUserModelId;
std::wstring pwaAppId;
std::wstring protocolPath;
bool canLaunchElevated = false;
bool IsEdge() const;
bool IsChrome() const;
bool IsSteamGame() const;
};
using AppList = std::vector<AppData>;

View File

@@ -0,0 +1,171 @@
#include "pch.h"
#include "SteamHelper.h"
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <filesystem>
#include <regex>
#include <string>
namespace Utils
{
static std::wstring Utf8ToWide(const std::string& utf8)
{
if (utf8.empty())
return L"";
int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
if (size <= 0)
return L"";
std::wstring wide(size, L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), wide.data(), size);
return wide;
}
namespace Steam
{
using namespace std;
namespace fs = std::filesystem;
static std::optional<std::wstring> GetSteamExePathFromRegistry()
{
static std::optional<std::wstring> cachedPath;
if (cachedPath.has_value())
{
return cachedPath;
}
const std::vector<HKEY> roots = { HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS };
const std::vector<std::wstring> subKeys = {
L"steam\\shell\\open\\command",
L"Software\\Classes\\steam\\shell\\open\\command",
};
for (HKEY root : roots)
{
for (const auto& subKey : subKeys)
{
HKEY hKey;
if (RegOpenKeyExW(root, subKey.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
wchar_t value[512];
DWORD size = sizeof(value);
DWORD type = 0;
if (RegQueryValueExW(hKey, nullptr, nullptr, &type, reinterpret_cast<LPBYTE>(value), &size) == ERROR_SUCCESS &&
(type == REG_SZ || type == REG_EXPAND_SZ))
{
std::wregex exeRegex(LR"delim("([^"]+steam\.exe)")delim");
std::wcmatch match;
if (std::regex_search(value, match, exeRegex) && match.size() > 1)
{
RegCloseKey(hKey);
cachedPath = match[1].str();
return cachedPath;
}
}
RegCloseKey(hKey);
}
}
}
cachedPath = std::nullopt;
return std::nullopt;
}
static fs::path GetSteamBasePath()
{
auto steamFolderOpt = GetSteamExePathFromRegistry();
if (!steamFolderOpt)
{
return {};
}
return fs::path(*steamFolderOpt).parent_path() / L"steamapps";
}
static fs::path GetAcfFilePath(const std::wstring& gameId)
{
auto steamFolderOpt = GetSteamExePathFromRegistry();
if (!steamFolderOpt)
{
return {};
}
return GetSteamBasePath() / (L"appmanifest_" + gameId + L".acf");
}
static fs::path GetGameInstallPath(const std::wstring& gameFolderName)
{
auto steamFolderOpt = GetSteamExePathFromRegistry();
if (!steamFolderOpt)
{
return {};
}
return GetSteamBasePath() / L"common" / gameFolderName;
}
static unordered_map<wstring, wstring> ParseAcfFile(const fs::path& acfPath)
{
unordered_map<wstring, wstring> result;
ifstream file(acfPath);
if (!file.is_open())
return result;
string line;
while (getline(file, line))
{
smatch matches;
static const regex pattern(R"delim("([^"]+)"\s+"([^"]+)")delim");
if (regex_search(line, matches, pattern) && matches.size() == 3)
{
wstring key = Utf8ToWide(matches[1].str());
wstring value = Utf8ToWide(matches[2].str());
result[key] = value;
}
}
return result;
}
std::unique_ptr<Steam::SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId)
{
fs::path acfPath = Steam::GetAcfFilePath(gameId);
if (!fs::exists(acfPath))
return nullptr;
auto kv = ParseAcfFile(acfPath);
if (kv.empty() || kv.find(L"installdir") == kv.end())
return nullptr;
fs::path gamePath = Steam::GetGameInstallPath(kv[L"installdir"]);
if (!fs::exists(gamePath))
return nullptr;
auto game = std::make_unique<Steam::SteamGame>();
game->gameId = gameId;
game->gameInstallationPath = gamePath.wstring();
return game;
}
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath)
{
const std::wstring steamGamePrefix = L"steam://rungameid/";
if (urlPath.rfind(steamGamePrefix, 0) == 0)
{
return urlPath.substr(steamGamePrefix.length());
}
return L"";
}
}
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include "pch.h"
namespace Utils
{
namespace NonLocalizable
{
const std::wstring AcfFileNameTemplate = L"appmanifest_<gameid>.acfs";
}
namespace Steam
{
struct SteamGame
{
std::wstring gameId;
std::wstring gameInstallationPath;
};
std::unique_ptr<SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId);
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath);
}
}

View File

@@ -41,6 +41,7 @@
<ClInclude Include="pch.h" />
<ClInclude Include="PwaHelper.h" />
<ClInclude Include="Result.h" />
<ClInclude Include="SteamHelper.h" />
<ClInclude Include="StringUtils.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WbemHelper.h" />
@@ -57,6 +58,7 @@
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="PwaHelper.cpp" />
<ClCompile Include="SteamGameHelper.cpp" />
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
<ClCompile Include="WbemHelper.cpp" />
<ClCompile Include="WorkspacesData.cpp" />

View File

@@ -53,6 +53,9 @@
<ClInclude Include="StringUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SteamHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -88,6 +91,9 @@
<ClCompile Include="WbemHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SteamGameHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -71,6 +71,8 @@ namespace SnapshotUtils
continue;
}
Logger::info("Try to get window app:{}", reinterpret_cast<void*>(window));
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
@@ -118,10 +120,19 @@ namespace SnapshotUtils
auto data = Utils::Apps::GetApp(processPath, pid, installedApps);
if (!data.has_value() || data->name.empty())
{
Logger::info(L"Installed app not found: {}", processPath);
Logger::info(L"Installed app not found:{},{}", reinterpret_cast<void*>(window), processPath);
continue;
}
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
{
// Only care about steam games if it has no thick frame to remain consistent with
// the behavior as before.
continue;
}
Logger::info(L"Found app for window:{},{}", reinterpret_cast<void*>(window), processPath);
auto appData = data.value();
bool isEdge = appData.IsEdge();

View File

@@ -200,6 +200,14 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
}
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
{
// Only care about steam games if it has no thick frame to remain consistent with
// the behavior as before.
continue;
}
if (!data.has_value())
{
continue;

View File

@@ -9,10 +9,12 @@ namespace WindowFilter
{
auto style = GetWindowLong(window, GWL_STYLE);
bool isPopup = WindowUtils::HasStyle(style, WS_POPUP);
bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME);
bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION);
bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX);
if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons)))
Logger::info("Style for window: {}, {:#x}", reinterpret_cast<void*>(window), style);
if (isPopup && !(hasCaption || hasMinimizeMaximizeButtons))
{
// popup windows we want to snap: e.g. Calculator, Telegram
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.

View File

@@ -121,4 +121,11 @@ namespace WindowUtils
return std::wstring(title);
}
inline bool HasThickFrame(HWND window)
{
auto style = GetWindowLong(window, GWL_STYLE);
return WindowUtils::HasStyle(style, WS_THICKFRAME);
}
}

View File

@@ -2,6 +2,7 @@
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Import Project="..\Microsoft.CmdPal.UI\CmdPal.pre.props" Condition="Exists('..\Microsoft.CmdPal.UI\CmdPal.pre.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
@@ -49,13 +50,20 @@
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>
EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
%(PreprocessorDefinitions);
</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'">
IS_DEV_BRANDING;%(PreprocessorDefinitions)
</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />

View File

@@ -3,6 +3,7 @@
#include <interface/powertoy_module_interface.h>
#include <atomic>
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
#include <common/SettingsAPI/settings_helpers.h>
@@ -10,10 +11,11 @@
#include <common/utils/resources.h>
#include <common/utils/package.h>
#include <common/utils/process_path.h>
#include <common/utils/winapi_error.h>
#include <common/interop/shared_constants.h>
#include <Psapi.h>
#include <TlHelp32.h>
#include <common/utils/winapi_error.h>
#include <thread>
HINSTANCE g_hInst_cmdPal = 0;
@@ -37,8 +39,6 @@ BOOL APIENTRY DllMain(HMODULE hInstance,
class CmdPal : public PowertoyModuleIface
{
private:
bool m_enabled = false;
std::wstring app_name;
//contains the non localized key of the powertoy
@@ -46,7 +46,10 @@ private:
HANDLE m_hTerminateEvent;
void LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
// Track if this is the first call to enable
bool firstEnableCall = true;
static bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, bool silentFail)
{
std::wstring dir = std::filesystem::path(appPath).parent_path();
@@ -54,6 +57,10 @@ private:
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.hwnd = nullptr;
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
if (silentFail)
{
sei.fMask = sei.fMask | SEE_MASK_FLAG_NO_UI;
}
sei.lpVerb = elevated ? L"runas" : L"open";
sei.lpFile = appPath.c_str();
sei.lpParameters = commandLineArgs.c_str();
@@ -64,7 +71,11 @@ private:
{
std::wstring error = get_last_error_or_default(GetLastError());
Logger::error(L"Failed to launch process. {}", error);
return false;
}
m_launched.store(true);
return true;
}
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
@@ -122,6 +133,9 @@ private:
}
public:
static std::atomic<bool> m_enabled;
static std::atomic<bool> m_launched;
CmdPal()
{
app_name = L"CmdPal";
@@ -133,10 +147,7 @@ public:
~CmdPal()
{
if (m_enabled)
{
}
m_enabled = false;
CmdPal::m_enabled.store(false);
}
// Destroy the powertoy and free memory
@@ -203,15 +214,17 @@ public:
{
Logger::trace("CmdPal::enable()");
m_enabled = true;
CmdPal::m_enabled.store(true);
try
{
std::wstring packageName = L"Microsoft.CommandPalette";
#ifdef _DEBUG
packageName = L"Microsoft.CommandPalette.Dev";
std::wstring packageName = L"Microsoft.CommandPalette";
std::wstring launchPath = L"x-cmdpal://background";
#ifdef IS_DEV_BRANDING
packageName = L"Microsoft.CommandPalette.Dev";
#endif
if (!package::GetRegisteredPackage(packageName, false).has_value())
if (!package::GetRegisteredPackage(packageName, false).has_value())
{
try
{
Logger::info(L"CmdPal not installed. Installing...");
@@ -238,19 +251,34 @@ public:
}
}
}
}
catch (std::exception& e)
{
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
errorMessage += e.what();
Logger::error(errorMessage);
catch (std::exception& e)
{
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
errorMessage += e.what();
Logger::error(errorMessage);
}
}
#if _DEBUG
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
#else
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
#endif
if (!package::GetRegisteredPackage(packageName, false).has_value())
{
Logger::error("Cmdpal is not registered, quit..");
return;
}
if (!firstEnableCall)
{
Logger::trace("Not first attempt, try to launch");
LaunchApp(launchPath, L"", false /*no elevated*/, false /*error pop up*/);
}
else
{
// If first time enable, do retry launch.
Logger::trace("First attempt, try to launch");
std::thread launchThread(&CmdPal::RetryLaunch, launchPath);
launchThread.detach();
}
firstEnableCall = false;
}
virtual void disable()
@@ -258,7 +286,44 @@ public:
Logger::trace("CmdPal::disable()");
TerminateCmdPal();
m_enabled = false;
CmdPal::m_enabled.store(false);
}
static void RetryLaunch(std::wstring path)
{
const int base_delay_milliseconds = 1000;
int max_retry = 9; // 2**9 - 1 seconds. Control total wait time within 10 min.
int retry = 0;
do
{
auto launch_result = LaunchApp(path, L"", false, retry < max_retry);
if (launch_result)
{
Logger::info(L"CmdPal launched successfully after {} retries.", retry);
return;
}
else
{
Logger::error(L"Retry {} launch CmdPal launch failed.", retry);
}
// When we got max retry, we don't need to wait for the next retry.
if (retry < max_retry)
{
int delay = base_delay_milliseconds * (1 << (retry));
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
}
++retry;
} while (retry <= max_retry && m_enabled.load() && !m_launched.load());
if (!m_enabled.load() || m_launched.load())
{
Logger::error(L"Retry cancelled. CmdPal is disabled or already launched.");
}
else
{
Logger::error(L"CmdPal launch failed after {} attempts.", retry);
}
}
virtual bool on_hotkey(size_t) override
@@ -273,11 +338,14 @@ public:
virtual bool is_enabled() override
{
return m_enabled;
return CmdPal::m_enabled.load();
}
};
std::atomic<bool> CmdPal::m_enabled{ false };
std::atomic<bool> CmdPal::m_launched{ false };
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new CmdPal();
}
}

View File

@@ -3,13 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.1.0" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.2.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250401001" />
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Text.Json" Version="9.0.3" />

View File

@@ -14,11 +14,12 @@ namespace TemplateCmdPalExtension;
public class Program
{
[MTAThread]
public static async Task Main(string[] args)
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
{
await using global::Shmuelie.WinRTServer.ComServer server = new();
global::Shmuelie.WinRTServer.ComServer server = new();
ManualResetEvent extensionDisposedEvent = new(false);
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
@@ -31,6 +32,8 @@ public class Program
// This will make the main thread wait until the event is signalled by the extension class.
// Since we have single instance of the extension object, we exit as soon as it is disposed.
extensionDisposedEvent.WaitOne();
server.Stop();
server.UnsafeDispose();
}
else
{

View File

@@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Common.Contracts;
public interface IFileService
{
T Read<T>(string folderPath, string fileName);
void Save<T>(string folderPath, string fileName, T content);
void Delete(string folderPath, string fileName);
}

View File

@@ -1,16 +0,0 @@
// 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;
namespace Microsoft.CmdPal.Common.Contracts;
public interface ILocalSettingsService
{
Task<bool> HasSettingAsync(string key);
Task<T?> ReadSettingAsync<T>(string key);
Task SaveSettingAsync<T>(string key, T value);
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using Microsoft.UI.Xaml;
namespace Microsoft.CmdPal.Common.Extensions;
/// <summary>
/// Extension class implementing extension methods for <see cref="Application"/>.
/// </summary>
public static class ApplicationExtensions
{
/// <summary>
/// Get registered services at the application level from anywhere in the
/// application.
///
/// Note:
/// https://learn.microsoft.com/uwp/api/windows.ui.xaml.application.current?view=winrt-22621#windows-ui-xaml-application-current
/// "Application is a singleton that implements the static Current property
/// to provide shared access to the Application instance for the current
/// application. The singleton pattern ensures that state managed by
/// Application, including shared resources and properties, is available
/// from a single, shared location."
///
/// Example of usage:
/// <code>
/// Application.Current.GetService<T>()
/// </code>
/// </summary>
/// <typeparam name="T">Service type.</typeparam>
/// <param name="application">Current application.</param>
/// <returns>Service reference.</returns>
public static T GetService<T>(this Application application)
where T : class
{
return (application as IApp)!.GetService<T>();
}
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.CmdPal.Common.Extensions;
public static class IHostExtensions
{
/// <summary>
/// <inheritdoc cref="ActivatorUtilities.CreateInstance(IServiceProvider, Type, object[])"/>
/// </summary>
public static T CreateInstance<T>(this IHost host, params object[] parameters)
{
return ActivatorUtilities.CreateInstance<T>(host.Services, parameters);
}
/// <summary>
/// Gets the service object for the specified type, or throws an exception
/// if type was not registered.
/// </summary>
/// <typeparam name="T">Service type</typeparam>
/// <param name="host">Host object</param>
/// <returns>Service object</returns>
/// <exception cref="ArgumentException">Throw an exception if the specified
/// type is not registered</exception>
public static T GetService<T>(this IHost host)
where T : class
{
if (host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
}

View File

@@ -1,36 +0,0 @@
// 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.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Common.Helpers;
public static class Json
{
public static async Task<T> ToObjectAsync<T>(string value)
{
if (typeof(T) == typeof(bool))
{
return (T)(object)bool.Parse(value);
}
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value));
return (await JsonSerializer.DeserializeAsync<T>(stream))!;
}
public static async Task<string> StringifyAsync<T>(T value)
{
if (typeof(T) == typeof(bool))
{
return value!.ToString()!.ToLowerInvariant();
}
await using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, value);
return Encoding.UTF8.GetString(stream.ToArray());
}
}

View File

@@ -9,7 +9,7 @@ using Microsoft.UI.Dispatching;
namespace Microsoft.CmdPal.Common.Helpers;
public static class NativeEventWaiter
public static partial class NativeEventWaiter
{
public static void WaitForEventLoop(string eventName, Action callback)
{

View File

@@ -9,7 +9,7 @@ using Windows.Win32.Foundation;
namespace Microsoft.CmdPal.Common.Helpers;
public static class RuntimeHelper
public static partial class RuntimeHelper
{
public static bool IsMSIX
{

View File

@@ -4,6 +4,6 @@
namespace Microsoft.CmdPal.Common.Messages;
public record HideWindowMessage()
public partial record HideWindowMessage()
{
}

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Common</RootNamespace>
<Nullable>enable</Nullable>

View File

@@ -1,18 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Common.Models;
public class LocalSettingsOptions
{
public string? ApplicationDataFolder
{
get; set;
}
public string? LocalSettingsFile
{
get; set;
}
}

View File

@@ -1,16 +1,7 @@
EnableWindow
CoCreateInstance
FileOpenDialog
FileSaveDialog
IFileOpenDialog
IFileSaveDialog
SHCreateItemFromParsingName
GetCurrentPackageFullName
SetWindowLong
GetWindowLong
WINDOW_EX_STYLE
SHLoadIndirectString
StrFormatByteSizeEx
SFBS_FLAGS
MAX_PATH
GetDpiForWindow

View File

@@ -1,48 +0,0 @@
// 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.IO;
using System.Text;
using System.Text.Json;
using Microsoft.CmdPal.Common.Contracts;
namespace Microsoft.CmdPal.Common.Services;
public class FileService : IFileService
{
private static readonly Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
#pragma warning disable CS8603 // Possible null reference return.
public T Read<T>(string folderPath, string fileName)
{
var path = Path.Combine(folderPath, fileName);
if (File.Exists(path))
{
using var fileStream = File.OpenText(path);
return JsonSerializer.Deserialize<T>(fileStream.BaseStream);
}
return default;
}
#pragma warning restore CS8603 // Possible null reference return.
public void Save<T>(string folderPath, string fileName, T content)
{
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
var fileContent = JsonSerializer.Serialize(content);
File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, _encoding);
}
public void Delete(string folderPath, string fileName)
{
if (fileName != null && File.Exists(Path.Combine(folderPath, fileName)))
{
File.Delete(Path.Combine(folderPath, fileName));
}
}
}

View File

@@ -1,18 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Common.Services;
/// <summary>
/// Interface for the current application singleton object exposing the API
/// that can be accessed from anywhere in the application.
/// </summary>
public interface IApp
{
/// <summary>
/// Gets services registered at the application level.
/// </summary>
public T GetService<T>()
where T : class;
}

View File

@@ -1,120 +0,0 @@
// 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.IO;
using System.Threading.Tasks;
using Microsoft.CmdPal.Common.Contracts;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Models;
using Microsoft.Extensions.Options;
using Windows.Storage;
namespace Microsoft.CmdPal.Common.Services;
public class LocalSettingsService : ILocalSettingsService
{
// TODO! for now, we're hardcoding the path as effectively:
// %localappdata%\CmdPal\LocalSettings.json
private const string DefaultApplicationDataFolder = "CmdPal";
private const string DefaultLocalSettingsFile = "LocalSettings.json";
private readonly IFileService _fileService;
private readonly LocalSettingsOptions _options;
private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
private readonly string _applicationDataFolder;
private readonly string _localSettingsFile;
private readonly bool _isMsix;
private Dictionary<string, object> _settings;
private bool _isInitialized;
public LocalSettingsService(IFileService fileService, IOptions<LocalSettingsOptions> options)
{
_isMsix = false; // RuntimeHelper.IsMSIX;
_fileService = fileService;
_options = options.Value;
_applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? DefaultApplicationDataFolder);
_localSettingsFile = _options.LocalSettingsFile ?? DefaultLocalSettingsFile;
_settings = new Dictionary<string, object>();
}
private async Task InitializeAsync()
{
if (!_isInitialized)
{
_settings = await Task.Run(() => _fileService.Read<Dictionary<string, object>>(_applicationDataFolder, _localSettingsFile)) ?? new Dictionary<string, object>();
_isInitialized = true;
}
}
public async Task<bool> HasSettingAsync(string key)
{
if (_isMsix)
{
return ApplicationData.Current.LocalSettings.Values.ContainsKey(key);
}
else
{
await InitializeAsync();
if (_settings != null)
{
return _settings.ContainsKey(key);
}
}
return false;
}
public async Task<T?> ReadSettingAsync<T>(string key)
{
if (_isMsix)
{
if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj))
{
return await Json.ToObjectAsync<T>((string)obj);
}
}
else
{
await InitializeAsync();
if (_settings != null && _settings.TryGetValue(key, out var obj))
{
var s = obj.ToString();
if (s != null)
{
return await Json.ToObjectAsync<T>(s);
}
}
}
return default;
}
public async Task SaveSettingAsync<T>(string key, T value)
{
if (_isMsix)
{
ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value!);
}
else
{
await InitializeAsync();
_settings[key] = await Json.StringifyAsync(value!);
await Task.Run(() => _fileService.Save(_applicationDataFolder, _localSettingsFile, _settings));
}
}
}

View File

@@ -49,7 +49,7 @@ public partial class AppStateModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, _deserializerOptions);
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -73,7 +73,7 @@ public partial class AppStateModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -89,7 +89,7 @@ public partial class AppStateModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
var serialized = savedSettings.ToJsonString(_serializerOptions);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -122,18 +122,19 @@ public partial class AppStateModel : ObservableObject
return Path.Combine(directory, "state.json");
}
private static readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
// private static readonly JsonSerializerOptions _serializerOptions = new()
// {
// WriteIndented = true,
// Converters = { new JsonStringEnumConverter() },
// };
private static readonly JsonSerializerOptions _deserializerOptions = new()
{
PropertyNameCaseInsensitive = true,
IncludeFields = true,
AllowTrailingCommas = true,
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
ReadCommentHandling = JsonCommentHandling.Skip,
};
// private static readonly JsonSerializerOptions _deserializerOptions = new()
// {
// PropertyNameCaseInsensitive = true,
// IncludeFields = true,
// AllowTrailingCommas = true,
// PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
// ReadCommentHandling = JsonCommentHandling.Skip,
// };
}

View File

@@ -4,18 +4,14 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>,
IRecipient<UpdateItemKeybindingsMessage>
IRecipient<UpdateCommandBarMessage>
{
public ICommandBarContext? SelectedItem
{
@@ -29,6 +25,8 @@ public partial class CommandBarViewModel : ObservableObject,
field = value;
SetSelectedItem(value);
OnPropertyChanged(nameof(SelectedItem));
}
}
@@ -51,20 +49,17 @@ public partial class CommandBarViewModel : ObservableObject,
public partial PageViewModel? CurrentPage { get; set; }
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> ContextCommands { get; set; } = [];
public partial ObservableCollection<ContextMenuStackViewModel> ContextMenuStack { get; set; } = [];
private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
public CommandBarViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
}
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
public void Receive(UpdateItemKeybindingsMessage message) => _contextKeybindings = message.Keys;
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
@@ -109,53 +104,97 @@ public partial class CommandBarViewModel : ObservableObject,
if (SelectedItem.MoreCommands.Count() > 1)
{
ShouldShowContextMenu = true;
ContextCommands = [.. SelectedItem.AllCommands];
ContextMenuStack.Clear();
ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
OnPropertyChanged(nameof(ContextMenu));
}
else
{
ShouldShowContextMenu = false;
}
OnPropertyChanged(nameof(HasSecondaryCommand));
OnPropertyChanged(nameof(SecondaryCommand));
OnPropertyChanged(nameof(ShouldShowContextMenu));
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in when an item in the list is tapped
[RelayCommand]
private void InvokeItem(CommandContextItemViewModel item) =>
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
// [RelayCommand]
public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
PerformCommand(item);
// this comes in when the primary button is tapped
public void InvokePrimaryCommand()
{
if (PrimaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
}
PerformCommand(SecondaryCommand);
}
// this comes in when the secondary button is tapped
public void InvokeSecondaryCommand()
{
if (SecondaryCommand != null)
PerformCommand(SecondaryCommand);
}
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
}
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
{
if (command == null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
return ContextKeybindingResult.Unhandled;
}
if (command.HasMoreCommands)
{
ContextMenuStack.Add(new ContextMenuStackViewModel(command));
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
return ContextKeybindingResult.KeepOpen;
}
else
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
return ContextKeybindingResult.Hide;
}
}
public bool CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
public bool CanPopContextStack()
{
if (_contextKeybindings != null)
return ContextMenuStack.Count > 1;
}
public void PopContextStack()
{
if (ContextMenuStack.Count > 1)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (_contextKeybindings.TryGetValue(pressedKeyChord, out var item))
{
// TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
// so that the correct item is activated.
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
return true;
}
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
return false;
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}
public void ClearContextStack()
{
while (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}
}
public enum ContextKeybindingResult
{
Unhandled,
Hide,
KeepOpen,
}

View File

@@ -48,7 +48,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
IEnumerable<CommandContextItemViewModel> ICommandBarContext.MoreCommands => MoreCommands;
IEnumerable<CommandContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
public bool HasMoreCommands => MoreCommands.Count > 0;
@@ -187,23 +187,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// use Initialize straight up
MoreCommands.ForEach(contextItem =>
{
contextItem.InitializeProperties();
contextItem.SlowInitializeProperties();
});
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
if (!string.IsNullOrEmpty(model.Command?.Name))
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
};
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
};
// Only set the icon on the context item for us if our command didn't
// have its own icon
if (!Command.HasIcon)
{
_defaultCommandContextItem._listItemIcon = _listItemIcon;
// Only set the icon on the context item for us if our command didn't
// have its own icon
if (!Command.HasIcon)
{
_defaultCommandContextItem._listItemIcon = _listItemIcon;
}
}
Initialized |= InitializedState.SelectionInitialized;
@@ -398,23 +401,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
internal Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}
[Flags]

View File

@@ -66,8 +66,10 @@ public sealed class CommandProviderWrapper
DisplayName = provider.DisplayName;
Icon = new(provider.Icon);
Icon.InitializeProperties();
// Note: explicitly not InitializeProperties()ing the settings here. If
// we do that, then we'd regress GH #38321
Settings = new(provider.Settings, this, _taskScheduler);
Settings.InitializeProperties();
Logger.LogDebug($"Initialized command provider {ProviderId}");
}
@@ -151,9 +153,11 @@ public sealed class CommandProviderWrapper
Icon = new(model.Icon);
Icon.InitializeProperties();
// Note: explicitly not InitializeProperties()ing the settings here. If
// we do that, then we'd regress GH #38321
Settings = new(model.Settings, this, _taskScheduler);
Settings.InitializeProperties();
// We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
@@ -194,21 +198,6 @@ public sealed class CommandProviderWrapper
}
}
/* This is a View/ExtensionHost piece
* public void AllowSetForeground(bool allow)
{
if (!IsExtension)
{
return;
}
var iextn = extensionWrapper?.GetExtensionObject();
unsafe
{
PInvoke.CoAllowSetForegroundWindow(iextn);
}
}*/
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
public override int GetHashCode() => _commandProvider.GetHashCode();

View File

@@ -2,18 +2,25 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
{
private readonly ExtensionObject<ICommandSettings> _model = new(_unsafeSettings);
public ContentPageViewModel? SettingsPage { get; private set; }
public void InitializeProperties()
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
private void UnsafeInitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
@@ -27,4 +34,27 @@ public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings,
SettingsPage.InitializeProperties();
}
}
public void SafeInitializeProperties()
{
try
{
UnsafeInitializeProperties();
}
catch (Exception ex)
{
Logger.LogError($"Failed to load settings page", ex: ex);
}
Initialized = true;
}
public void DoOnUiThread(Action action)
{
Task.Factory.StartNew(
action,
CancellationToken.None,
TaskCreationOptions.None,
mainThread);
}
}

View File

@@ -13,12 +13,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
{
public CreatedExtensionForm(string name, string displayName, string path)
{
var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
TemplateJson = CardTemplate;
DataJson = $$"""
{
"name": {{JsonSerializer.Serialize(name)}},
"directory": {{JsonSerializer.Serialize(path)}},
"displayName": {{JsonSerializer.Serialize(displayName)}}
"name": {{serializeString(name)}},
"directory": {{serializeString(path)}},
"displayName": {{serializeString(displayName)}}
}
""";
_name = name;
@@ -28,13 +29,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
public override ICommandResult SubmitForm(string inputs, string data)
{
JsonObject? dataInput = JsonNode.Parse(data)?.AsObject();
var dataInput = JsonNode.Parse(data)?.AsObject();
if (dataInput == null)
{
return CommandResult.KeepOpen();
}
string verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
var verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
return verb switch
{
"sln" => OpenSolution(),
@@ -47,7 +48,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenSolution()
{
string[] parts = [_path, _name, $"{_name}.sln"];
string pathToSolution = Path.Combine(parts);
var pathToSolution = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToSolution);
return CommandResult.Hide();
}
@@ -55,7 +56,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenDirectory()
{
string[] parts = [_path, _name];
string pathToDir = Path.Combine(parts);
var pathToDir = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToDir);
return CommandResult.Hide();
}

View File

@@ -194,9 +194,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
Directory.Delete(tempDir, true);
}
private string FormatJsonString(string str)
{
private string FormatJsonString(string str) =>
// Escape the string for JSON
return JsonSerializer.Serialize(str);
}
JsonSerializer.Serialize(str, JsonSerializationContext.Default.String);
}

View File

@@ -6,6 +6,7 @@ using System.Text.Json;
using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Templating;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
@@ -28,42 +29,67 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
public AdaptiveCardParseResult? Card { get; private set; }
private static string Serialize(string? s) =>
JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
private static bool TryBuildCard(
string templateJson,
string dataJson,
out AdaptiveCardParseResult? card,
out Exception? error)
{
card = null;
error = null;
try
{
var template = new AdaptiveCardTemplate(templateJson);
var cardJson = template.Expand(dataJson);
card = AdaptiveCard.FromJsonString(cardJson);
return true;
}
catch (Exception ex)
{
Logger.LogError("Error building card from template: {Message}", ex.Message);
error = ex;
return false;
}
}
public override void InitializeProperties()
{
var model = _formModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}
try
{
TemplateJson = model.TemplateJson;
StateJson = model.StateJson;
DataJson = model.DataJson;
TemplateJson = model.TemplateJson;
StateJson = model.StateJson;
DataJson = model.DataJson;
AdaptiveCardTemplate template = new(TemplateJson);
var cardJson = template.Expand(DataJson);
Card = AdaptiveCard.FromJsonString(cardJson);
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
{
Card = builtCard;
UpdateProperty(nameof(Card));
return;
}
catch (Exception e)
{
// If we fail to parse the card JSON, then display _our own card_
// with the exception
AdaptiveCardTemplate template = new(ErrorCardJson);
// todo: we could probably stick Card.Errors in there too
var dataJson = $$"""
{
"error_message": {{JsonSerializer.Serialize(e.Message)}},
"error_stack": {{JsonSerializer.Serialize(e.StackTrace)}},
"inner_exception": {{JsonSerializer.Serialize(e.InnerException?.Message)}},
"template_json": {{JsonSerializer.Serialize(TemplateJson)}},
"data_json": {{JsonSerializer.Serialize(DataJson)}}
}
""";
var cardJson = template.Expand(dataJson);
Card = AdaptiveCard.FromJsonString(cardJson);
var errorPayload = $$"""
{
"error_message": {{Serialize(renderingError!.Message)}},
"error_stack": {{Serialize(renderingError.StackTrace)}},
"inner_exception": {{Serialize(renderingError.InnerException?.Message)}},
"template_json": {{Serialize(TemplateJson)}},
"data_json": {{Serialize(DataJson)}}
}
""";
if (TryBuildCard(ErrorCardJson, errorPayload, out var errorCard, out var _))
{
Card = errorCard;
UpdateProperty(nameof(Card));
return;
}
UpdateProperty(nameof(Card));

View File

@@ -167,7 +167,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
Commands.ForEach(contextItem =>
{
contextItem.InitializeProperties();
contextItem.SlowInitializeProperties();
});
}
else

View File

@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContextMenuStackViewModel : ObservableObject
{
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> FilteredItems { get; set; }
private readonly IContextMenuContext _context;
private string _lastSearchText = string.Empty;
// private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
public ContextMenuStackViewModel(IContextMenuContext context)
{
_context = context;
FilteredItems = [.. context.AllCommands];
}
public void SetSearchText(string searchText)
{
if (searchText == _lastSearchText)
{
return;
}
_lastSearchText = searchText;
var commands = _context.AllCommands.Where(c => c.ShouldBeVisible);
if (string.IsNullOrEmpty(searchText))
{
ListHelpers.InPlaceUpdateList(FilteredItems, commands);
return;
}
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
}
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
{
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
{
return 1;
}
if (string.IsNullOrEmpty(item.Title))
{
return 0;
}
var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
}
public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = _context.Keybindings();
if (keybindings != 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 item))
{
return item;
}
}
return null;
}
}

View File

@@ -61,6 +61,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
private Task? _initializeItemsTask;
private CancellationTokenSource? _cancellationTokenSource;
private ListItemViewModel? _lastSelectedItem;
public override bool IsInitialized
{
get => base.IsInitialized; protected set
@@ -328,7 +330,24 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
[RelayCommand]
private void UpdateSelectedItem(ListItemViewModel item)
private void UpdateSelectedItem(ListItemViewModel? item)
{
if (_lastSelectedItem != null)
{
_lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
if (item != null)
{
SetSelectedItem(item);
}
else
{
ClearSelectedItem();
}
}
private void SetSelectedItem(ListItemViewModel item)
{
if (!item.SafeSlowInit())
{
@@ -344,8 +363,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(item.Keybindings()));
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
@@ -357,6 +374,60 @@ public partial class ListViewModel : PageViewModel, IDisposable
TextToSuggest = item.TextToSuggest;
});
_lastSelectedItem = item;
_lastSelectedItem.PropertyChanged += SelectedItemPropertyChanged;
}
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var item = _lastSelectedItem;
if (item == null)
{
return;
}
// already on the UI thread here
switch (e.PropertyName)
{
case nameof(item.Command):
case nameof(item.SecondaryCommand):
case nameof(item.AllCommands):
case nameof(item.Name):
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
break;
case nameof(item.Details):
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
}
else
{
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
}
break;
case nameof(item.TextToSuggest):
TextToSuggest = item.TextToSuggest;
break;
}
}
private void ClearSelectedItem()
{
// GH #322:
// For inexplicable reasons, if you try updating the command bar and
// the details on the same UI thread tick as updating the list, we'll
// explode
DoOnUiThread(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
TextToSuggest = string.Empty;
});
}
public override void InitializeProperties()
@@ -436,7 +507,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
break;
case nameof(EmptyContent):
EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.InitializeProperties();
EmptyContent.SlowInitializeProperties();
break;
case nameof(IsLoading):
UpdateEmptyContent();
@@ -454,6 +525,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
return;
}
UpdateProperty(nameof(EmptyContent));
DoOnUiThread(
() =>
{

View File

@@ -2,8 +2,11 @@
// 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.CommandPalette.Extensions;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record UpdateItemKeybindingsMessage(Dictionary<KeyChord, CommandContextItemViewModel>? Keys);
public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
{
public bool Handled { get; set; }
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
@@ -13,22 +14,42 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
{
}
// Represents everything the command bar needs to know about to show command
// buttons at the bottom.
//
// This is implemented by both ListItemViewModel and ContentPageViewModel,
// the two things with sub-commands.
public interface ICommandBarContext : INotifyPropertyChanged
public interface IContextMenuContext : INotifyPropertyChanged
{
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
public bool HasMoreCommands { get; }
public List<CommandContextItemViewModel> AllCommands { get; }
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}
// Represents everything the command bar needs to know about to show command
// buttons at the bottom.
//
// This is implemented by both ListItemViewModel and ContentPageViewModel,
// the two things with sub-commands.
public interface ICommandBarContext : IContextMenuContext
{
public string SecondaryCommandName { get; }
public CommandItemViewModel? PrimaryCommand { get; }
public CommandItemViewModel? SecondaryCommand { get; }
public List<CommandContextItemViewModel> AllCommands { get; }
}

View File

@@ -1,5 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -67,4 +69,15 @@
</Content>
</ItemGroup>
<!-- Just mark it as AOT compatible. Do not publish with AOT now. We need fully test before we really publish it as AOT enabled-->
<!--<PropertyGroup>
<SelfContained>true</SelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<PublishTrimmed>true</PublishTrimmed>
<PublishSingleFile>true</PublishSingleFile>
--><!-- <DisableRuntimeMarshalling>true</DisableRuntimeMarshalling> --><!--
<PublishAot>true</PublishAot>
<EnableMsixTooling>true</EnableMsixTooling>
</PropertyGroup>-->
</Project>

View File

@@ -11,7 +11,7 @@ using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionService : IExtensionService, IDisposable
public partial class ExtensionService : IExtensionService, IDisposable
{
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;

View File

@@ -12,6 +12,7 @@ using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
// [assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionWrapper : IExtensionWrapper
@@ -113,25 +114,36 @@ public class ExtensionWrapper : IExtensionWrapper
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
if (hr.Value == -2147024893)
unsafe
{
Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
// We don't really need to throw this exception.
// We'll just return out nothing.
return;
if (hr.Value == -2147024893)
{
Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
// We don't really need to throw this exception.
// We'll just return out nothing.
return;
}
extensionPtr = Marshal.GetIUnknownForObject((nint)extensionObj);
if (hr < 0)
{
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
Marshal.ThrowExceptionForHR(hr);
}
// extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
extensionPtr = (nint)extensionObj;
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
}
extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
if (hr < 0)
{
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
Marshal.ThrowExceptionForHR(hr);
}
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
}
finally
{

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false
}

View File

@@ -99,7 +99,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
//// Run on background thread from ListPage.xaml.cs
[RelayCommand]
private Task<bool> InitializeAsync()
internal Task<bool> InitializeAsync()
{
// TODO: We may want a SemaphoreSlim lock here.
@@ -182,6 +182,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
return; // throw?
}
var updateProperty = true;
switch (propertyName)
{
case nameof(Name):
@@ -198,9 +199,21 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
case nameof(Icon):
this.Icon = new(model.Icon);
break;
default:
updateProperty = false;
break;
}
UpdateProperty(propertyName);
// GH #38829: If we always UpdateProperty here, then there's a possible
// race condition, where we raise the PropertyChanged(SearchText)
// before the subclass actually retrieves the new SearchText from the
// model. In that race situation, if the UI thread handles the
// PropertyChanged before ListViewModel fetches the SearchText, it'll
// think that the old search text is the _new_ value.
if (updateProperty)
{
UpdateProperty(propertyName);
}
}
public new void ShowException(Exception ex, string? extensionHint = null)

View File

@@ -18,6 +18,8 @@ public partial class ProviderSettingsViewModel(
IServiceProvider _serviceProvider) : ObservableObject
{
private readonly SettingsModel _settings = _serviceProvider.GetService<SettingsModel>()!;
private readonly Lock _initializeSettingsLock = new();
private Task? _initializeSettingsTask;
public string DisplayName => _provider.DisplayName;
@@ -34,6 +36,9 @@ public partial class ProviderSettingsViewModel(
public IconInfoViewModel Icon => _provider.Icon;
[ObservableProperty]
public partial bool LoadingSettings { get; set; } = _provider.Settings?.HasSettings ?? false;
public bool IsEnabled
{
get => _providerSettings.IsEnabled;
@@ -56,15 +61,60 @@ public partial class ProviderSettingsViewModel(
}
}
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
/// <summary>
/// Gets a value indicating whether returns true if we have a settings page
/// that's initialized, or we are still working on initializing that
/// settings page. If we don't have a settings object, or that settings
/// object doesn't have a settings page, then we'll return false.
/// </summary>
public bool HasSettings
{
OnPropertyChanged(nameof(ExtensionSubtext));
OnPropertyChanged(nameof(TopLevelCommands));
get
{
if (_provider.Settings == null)
{
return false;
}
if (_provider.Settings.Initialized)
{
return _provider.Settings.HasSettings;
}
// settings still need to be loaded.
return LoadingSettings;
}
}
public bool HasSettings => _provider.Settings != null && _provider.Settings.SettingsPage != null;
/// <summary>
/// Gets will return the settings page, if we have one, and have initialized it.
/// If we haven't initialized it, this will kick off a thread to start
/// initializing it.
/// </summary>
public ContentPageViewModel? SettingsPage
{
get
{
if (_provider.Settings == null)
{
return null;
}
public ContentPageViewModel? SettingsPage => HasSettings ? _provider?.Settings?.SettingsPage : null;
if (_provider.Settings.Initialized)
{
LoadingSettings = false;
return _provider.Settings.SettingsPage;
}
// Don't load the settings if we're already working on it
lock (_initializeSettingsLock)
{
_initializeSettingsTask ??= Task.Run(InitializeSettingsPage);
}
return null;
}
}
[field: AllowNull]
public List<TopLevelViewModel> TopLevelCommands
@@ -90,4 +140,30 @@ public partial class ProviderSettingsViewModel(
}
private void Save() => SettingsModel.SaveSettings(_settings);
private void InitializeSettingsPage()
{
if (_provider.Settings == null)
{
return;
}
_provider.Settings.SafeInitializeProperties();
_provider.Settings.DoOnUiThread(() =>
{
// Changing these properties will try to update XAML, and that has
// to be handled on the UI thread, so we need to raise them on the
// UI thread
LoadingSettings = false;
OnPropertyChanged(nameof(HasSettings));
OnPropertyChanged(nameof(LoadingSettings));
OnPropertyChanged(nameof(SettingsPage));
});
}
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
{
OnPropertyChanged(nameof(ExtensionSubtext));
OnPropertyChanged(nameof(TopLevelCommands));
}
}

View File

@@ -10,7 +10,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class RecentCommandsManager : ObservableObject
{
[JsonInclude]
private List<HistoryItem> History { get; set; } = [];
internal List<HistoryItem> History { get; set; } = [];
public RecentCommandsManager()
{

View File

@@ -40,6 +40,8 @@ public partial class SettingsModel : ObservableObject
public bool ShowSystemTrayIcon { get; set; } = true;
public bool IgnoreShortcutWhenFullscreen { get; set; } = true;
public Dictionary<string, ProviderSettings> ProviderSettings { get; set; } = [];
public Dictionary<string, CommandAlias> Aliases { get; set; } = [];
@@ -91,7 +93,7 @@ public partial class SettingsModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, _deserializerOptions);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -115,7 +117,7 @@ public partial class SettingsModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.SettingsModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -131,7 +133,7 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
var serialized = savedSettings.ToJsonString(_serializerOptions);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -164,19 +166,34 @@ public partial class SettingsModel : ObservableObject
return Path.Combine(directory, "settings.json");
}
private static readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
// private static readonly JsonSerializerOptions _serializerOptions = new()
// {
// WriteIndented = true,
// Converters = { new JsonStringEnumConverter() },
// };
// private static readonly JsonSerializerOptions _deserializerOptions = new()
// {
// PropertyNameCaseInsensitive = true,
// IncludeFields = true,
// Converters = { new JsonStringEnumConverter() },
// AllowTrailingCommas = true,
// };
}
private static readonly JsonSerializerOptions _deserializerOptions = new()
{
PropertyNameCaseInsensitive = true,
IncludeFields = true,
Converters = { new JsonStringEnumConverter() },
AllowTrailingCommas = true,
};
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(HistoryItem))]
[JsonSerializable(typeof(SettingsModel))]
[JsonSerializable(typeof(AppStateModel))]
[JsonSerializable(typeof(List<HistoryItem>), TypeInfoPropertyName = "HistoryList")]
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
internal sealed partial class JsonSerializationContext : JsonSerializerContext
{
}
public enum MonitorBehavior

View File

@@ -108,6 +108,16 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public bool IgnoreShortcutWhenFullscreen
{
get => _settings.IgnoreShortcutWhenFullscreen;
set
{
_settings.IgnoreShortcutWhenFullscreen = value;
Save();
}
}
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)

View File

@@ -109,9 +109,12 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>
_ = Task.Factory.StartNew(
() =>
async () =>
{
var result = (bool)viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
// bool f = await viewModel.InitializeCommand.ExecutionTask.;
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault<bool?>()!;
var result = await viewModel.InitializeAsync();
CurrentPage = viewModel; // result ? viewModel : null;
////LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error;

View File

@@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -44,6 +45,9 @@ public partial class TopLevelCommandManager : ObservableObject,
public async Task<bool> LoadBuiltinsAsync()
{
var s = new Stopwatch();
s.Start();
_builtInCommands.Clear();
// Load built-In commands first. These are all in-proc, and
@@ -53,53 +57,48 @@ public partial class TopLevelCommandManager : ObservableObject,
{
CommandProviderWrapper wrapper = new(provider, _taskScheduler);
_builtInCommands.Add(wrapper);
await LoadTopLevelCommandsFromProvider(wrapper);
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
lock (TopLevelCommands)
{
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
}
s.Stop();
Logger.LogDebug($"Loading built-ins took {s.ElapsedMilliseconds}ms");
return true;
}
// May be called from a background thread
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
WeakReference<IPageContext> weakSelf = new(this);
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
var settings = _serviceProvider.GetService<SettingsModel>()!;
var makeAndAdd = (ICommandItem? i, bool fallback) =>
List<TopLevelViewModel> commands = [];
foreach (var item in commandProvider.TopLevelItems)
{
var commandItemViewModel = new CommandItemViewModel(new(i), weakSelf);
var topLevelViewModel = new TopLevelViewModel(commandItemViewModel, fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, settings, _serviceProvider);
commands.Add(item);
}
lock (TopLevelCommands)
{
TopLevelCommands.Add(topLevelViewModel);
}
};
await Task.Factory.StartNew(
() =>
{
lock (TopLevelCommands)
{
foreach (var item in commandProvider.TopLevelItems)
{
TopLevelCommands.Add(item);
}
foreach (var item in commandProvider.FallbackItems)
{
TopLevelCommands.Add(item);
}
}
},
CancellationToken.None,
TaskCreationOptions.None,
_taskScheduler);
foreach (var item in commandProvider.FallbackItems)
{
commands.Add(item);
}
commandProvider.CommandsChanged -= CommandProvider_CommandsChanged;
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
return commands;
}
// By all accounts, we're already on a background thread (the COM call
@@ -239,25 +238,71 @@ public partial class TopLevelCommandManager : ObservableObject,
private async Task StartExtensionsAndGetCommands(IEnumerable<IExtensionWrapper> extensions)
{
// TODO This most definitely needs a lock
foreach (var extension in extensions)
{
Logger.LogDebug($"Starting {extension.PackageFullName}");
try
{
// start it ...
await extension.StartExtensionAsync();
var timer = new Stopwatch();
timer.Start();
// ... and fetch the command provider from it.
CommandProviderWrapper wrapper = new(extension, _taskScheduler);
_extensionCommandProviders.Add(wrapper);
await LoadTopLevelCommandsFromProvider(wrapper);
}
catch (Exception ex)
// Start all extensions in parallel
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();
foreach (var wrapper in wrappers)
{
_extensionCommandProviders.Add(wrapper!);
}
// 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();
lock (TopLevelCommands)
{
foreach (var commands in commandSets)
{
Logger.LogError(ex.ToString());
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
}
timer.Stop();
Logger.LogDebug($"Loading extensions took {timer.ElapsedMilliseconds} ms");
}
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
{
Logger.LogDebug($"Starting {extension.PackageFullName}");
try
{
await extension.StartExtensionAsync().WaitAsync(TimeSpan.FromSeconds(10));
return new CommandProviderWrapper(extension, _taskScheduler);
}
catch (Exception ex)
{
Logger.LogError($"Failed to start extension {extension.PackageFullName}: {ex}");
return null; // Return null for failed extensions
}
}
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
{
try
{
return await LoadTopLevelCommandsFromProvider(wrapper!).WaitAsync(TimeSpan.FromSeconds(10));
}
catch (TimeoutException)
{
Logger.LogError($"Loading commands from {wrapper!.ExtensionHost?.Extension?.PackageFullName} timed out");
}
catch (Exception ex)
{
Logger.LogError($"Failed to load commands for extension {wrapper!.ExtensionHost?.Extension?.PackageFullName}: {ex}");
}
return null;
}
private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)

View File

@@ -73,26 +73,12 @@ public partial class App : Application
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
AppWindow = new MainWindow();
var cmdArgs = Environment.GetCommandLineArgs();
var runFromPT = false;
foreach (var arg in cmdArgs)
{
if (arg == "RunFromPT")
{
runFromPT = true;
break;
}
}
if (!runFromPT)
{
AppWindow.Activate();
}
var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
((MainWindow)AppWindow).HandleLaunch(activatedEventArgs);
}
/// <summary>

View File

@@ -71,9 +71,9 @@
Padding="4"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
@@ -133,6 +133,8 @@
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
<StackPanel
Grid.Column="2"
@@ -223,27 +225,42 @@
ToolTipService.ToolTip="Ctrl+K"
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
<Button.Flyout>
<Flyout Placement="TopEdgeAlignedRight">
<ListView
x:Name="CommandsDropdown"
MinWidth="248"
Margin="-16,-12,-16,-12"
IsItemClickEnabled="True"
ItemClick="CommandsDropdown_ItemClick"
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}"
KeyDown="CommandsDropdown_KeyDown"
SelectionMode="None">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="12,7,12,7" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
<Flyout
Closing="Flyout_Closing"
Opened="Flyout_Opened"
Placement="TopEdgeAlignedRight">
<StackPanel>
<ListView
x:Name="CommandsDropdown"
MinWidth="248"
Margin="-16,-12,-16,-12"
IsItemClickEnabled="True"
ItemClick="CommandsDropdown_ItemClick"
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ContextMenu.FilteredItems, Mode=OneWay}"
KeyDown="CommandsDropdown_KeyDown"
SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="12,7,12,7" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
<TextBox
x:Name="ContextFilterBox"
x:Uid="ContextFilterBox"
Margin="-12,12,-12,-12"
KeyDown="ContextFilterBox_KeyDown"
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
TextChanged="ContextFilterBox_TextChanged" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>

View File

@@ -18,9 +18,10 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandBar : UserControl,
IRecipient<OpenContextMenuMessage>,
IRecipient<TryCommandKeybindingMessage>,
ICurrentPageAware
{
public CommandBarViewModel ViewModel { get; set; } = new();
public CommandBarViewModel ViewModel { get; } = new();
public PageViewModel? CurrentPageViewModel
{
@@ -38,6 +39,9 @@ public sealed partial class CommandBar : UserControl,
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
public void Receive(OpenContextMenuMessage message)
@@ -52,8 +56,41 @@ public sealed partial class CommandBar : UserControl,
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
CommandsDropdown.SelectedIndex = 0;
CommandsDropdown.Focus(FocusState.Programmatic);
UpdateUiForStackChange();
}
public void Receive(TryCommandKeybindingMessage msg)
{
if (!ViewModel.ShouldShowContextMenu)
{
return;
}
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
if (result == ContextKeybindingResult.Hide)
{
msg.Handled = true;
}
else if (result == ContextKeybindingResult.KeepOpen)
{
if (!MoreCommandsButton.Flyout.IsOpen)
{
var options = new FlyoutShowOptions
{
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
}
UpdateUiForStackChange();
msg.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
{
msg.Handled = false;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
@@ -88,8 +125,14 @@ public sealed partial class CommandBar : UserControl,
{
if (e.ClickedItem is CommandContextItemViewModel item)
{
ViewModel?.InvokeItemCommand.Execute(item);
MoreCommandsButton.Flyout.Hide();
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
{
MoreCommandsButton.Flyout.Hide();
}
else
{
UpdateUiForStackChange();
}
}
}
@@ -106,9 +149,136 @@ public sealed partial class CommandBar : UserControl,
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key) ?? false)
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
if (result == ContextKeybindingResult.Hide)
{
e.Handled = true;
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
else if (result == ContextKeybindingResult.KeepOpen)
{
e.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
{
e.Handled = false;
}
}
private void Flyout_Opened(object sender, object e)
{
UpdateUiForStackChange();
}
private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
{
ViewModel?.ClearContextStack();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var prop = e.PropertyName;
if (prop == nameof(ViewModel.ContextMenu))
{
UpdateUiForStackChange();
}
}
private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
{
ViewModel.ContextMenu?.SetSearchText(ContextFilterBox.Text);
if (CommandsDropdown.SelectedIndex == -1)
{
CommandsDropdown.SelectedIndex = 0;
}
}
private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (e.Key == VirtualKey.Enter)
{
if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
{
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
{
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
else
{
UpdateUiForStackChange();
}
e.Handled = true;
}
}
else if (e.Key == VirtualKey.Escape ||
(e.Key == VirtualKey.Left && altPressed))
{
if (ViewModel.CanPopContextStack())
{
ViewModel.PopContextStack();
UpdateUiForStackChange();
}
else
{
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
e.Handled = true;
}
CommandsDropdown_KeyDown(sender, e);
}
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Up)
{
// navigate previous
if (CommandsDropdown.SelectedIndex > 0)
{
CommandsDropdown.SelectedIndex--;
}
else
{
CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
}
e.Handled = true;
}
else if (e.Key == VirtualKey.Down)
{
// navigate next
if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
{
CommandsDropdown.SelectedIndex++;
}
else
{
CommandsDropdown.SelectedIndex = 0;
}
e.Handled = true;
}
}
private void UpdateUiForStackChange()
{
ContextFilterBox.Text = string.Empty;
ViewModel.ContextMenu?.SetSearchText(string.Empty);
CommandsDropdown.SelectedIndex = 0;
ContextFilterBox.Focus(FocusState.Programmatic);
}
}

View File

@@ -8,8 +8,6 @@ using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
@@ -23,7 +21,6 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl,
IRecipient<GoHomeMessage>,
IRecipient<FocusSearchBoxMessage>,
IRecipient<UpdateItemKeybindingsMessage>,
ICurrentPageAware
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -34,8 +31,6 @@ public sealed partial class SearchBar : UserControl,
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private bool _isBackspaceHeld;
private Dictionary<KeyChord, CommandContextItemViewModel>? _keyBindings;
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
@@ -74,7 +69,6 @@ public sealed partial class SearchBar : UserControl,
this.InitializeComponent();
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
}
public void ClearSearch()
@@ -173,17 +167,14 @@ public sealed partial class SearchBar : UserControl,
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
}
if (_keyBindings != null)
if (!e.Handled)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrlPressed, altPressed, shiftPressed, winPressed, (int)e.Key, 0);
if (_keyBindings.TryGetValue(pressedKeyChord, out var item))
{
// TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
// so that the correct item is activated.
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
e.Handled = true;
}
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
}
}
@@ -302,10 +293,5 @@ public sealed partial class SearchBar : UserControl,
public void Receive(GoHomeMessage message) => ClearSearch();
public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
public void Receive(UpdateItemKeybindingsMessage message)
{
_keyBindings = message.Keys;
}
public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}

View File

@@ -138,14 +138,15 @@
</controls:Case>
<controls:Case Value="True">
<StackPanel
Margin="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical"
Spacing="4">
<cpcontrols:IconBox
x:Name="IconBorder"
Width="56"
Height="56"
Width="48"
Height="48"
Margin="8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
@@ -154,11 +155,15 @@
Margin="0,4,0,0"
HorizontalAlignment="Center"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}" />
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}" />
Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap" />
</StackPanel>
</controls:Case>
</controls:SwitchPresenter>

View File

@@ -124,14 +124,12 @@ public sealed partial class ListPage : Page,
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
private void ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ItemsList.SelectedItem is ListItemViewModel item)
var vm = ViewModel;
var li = ItemsList.SelectedItem as ListItemViewModel;
_ = Task.Run(() =>
{
var vm = ViewModel;
_ = Task.Run(() =>
{
vm?.UpdateSelectedItemCommand.Execute(item);
});
}
vm?.UpdateSelectedItemCommand.Execute(li);
});
// There's mysterious behavior here, where the selection seemingly
// changes to _nothing_ when we're backspacing to a single character.

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using System.Security;
namespace Microsoft.CmdPal.UI.Helpers;
[SuppressUnmanagedCodeSecurity]
internal static class NativeMethods
{
[DllImport("shell32.dll")]
public static extern int SHQueryUserNotificationState(out UserNotificationState state);
}
internal enum UserNotificationState : int
{
QUNS_NOT_PRESENT = 1,
QUNS_BUSY,
QUNS_RUNNING_D3D_FULL_SCREEN,
QUNS_PRESENTATION_MODE,
QUNS_ACCEPTS_NOTIFICATIONS,
QUNS_QUIET_TIME,
QUNS_APP,
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.UI.Helpers;
internal sealed partial class WindowHelper
{
public static bool IsWindowFullscreen()
{
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 (state == UserNotificationState.QUNS_RUNNING_D3D_FULL_SCREEN ||
state == UserNotificationState.QUNS_BUSY ||
state == UserNotificationState.QUNS_PRESENTATION_MODE)
{
return true;
}
}
return false;
}
}

View File

@@ -1,4 +1,4 @@
<Window
<winuiex:WindowEx
x:Class="Microsoft.CmdPal.UI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -6,8 +6,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="using:Microsoft.CmdPal.UI.Pages"
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
xmlns:winuiex="using:WinUIEx"
Width="800"
Height="480"
MinWidth="320"
MinHeight="240"
Activated="MainWindow_Activated"
Closed="MainWindow_Closed"
mc:Ignorable="d">
<pages:ShellPage x:Name="RootShellPage" />
</Window>
</winuiex:WindowEx>

View File

@@ -19,21 +19,25 @@ using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
using Windows.UI;
using Windows.UI.WindowManagement;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CmdPal.UI;
public sealed partial class MainWindow : Window,
public sealed partial class MainWindow : WindowEx,
IRecipient<DismissMessage>,
IRecipient<ShowWindowMessage>,
IRecipient<HideWindowMessage>,
@@ -43,6 +47,7 @@ public sealed partial class MainWindow : Window,
private readonly WNDPROC? _hotkeyWndProc;
private readonly WNDPROC? _originalWndProc;
private readonly List<TopLevelHotkey> _hotkeys = [];
private bool _ignoreHotKeyWhenFullScreen = true;
// Stylistically, window messages are WM_*
#pragma warning disable SA1310 // Field names should not contain underscore
@@ -81,7 +86,6 @@ public sealed partial class MainWindow : Window,
this.SetIcon();
AppWindow.Title = RS_.GetString("AppName");
AppWindow.Resize(new SizeInt32 { Width = 1000, Height = 620 });
PositionCentered();
SetAcrylic();
@@ -157,6 +161,8 @@ public sealed partial class MainWindow : Window,
SetupHotkey(settings);
SetupTrayIcon(settings.ShowSystemTrayIcon);
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
// This will prevent our window from appearing in alt+tab or the taskbar.
// You'll _need_ to use the hotkey to summon it.
AppWindow.IsShownInSwitchers = System.Diagnostics.Debugger.IsAttached;
@@ -229,6 +235,16 @@ public sealed partial class MainWindow : Window,
PositionCentered(display);
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW);
// instead of showing the window, uncloak it from DWM
// This will make it visible to the user, without the animation or frames for
// loading XAML with composition
unsafe
{
BOOL value = false;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
}
PInvoke.SetForegroundWindow(hwnd);
PInvoke.SetActiveWindow(hwnd);
}
@@ -286,7 +302,7 @@ public sealed partial class MainWindow : Window,
ShowHwnd(message.Hwnd, settings.SummonOn);
}
public void Receive(HideWindowMessage message) => PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
public void Receive(HideWindowMessage message) => HideWindow();
public void Receive(QuitMessage message) =>
@@ -294,7 +310,21 @@ public sealed partial class MainWindow : Window,
DispatcherQueue.TryEnqueue(() => Close());
public void Receive(DismissMessage message) =>
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
HideWindow();
private void HideWindow()
{
// Hide our window
// Instead of hiding the window, cloak it from DWM
// This will make it invisible to the user, such that we can show it again
// by uncloaking it, which avoids an unnecessary "flicker in" that XAML does
unsafe
{
BOOL value = true;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
}
}
internal void MainWindow_Closed(object sender, WindowEventArgs args)
{
@@ -383,7 +413,9 @@ public sealed partial class MainWindow : Window,
return;
}
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// This will DWM cloak our window:
HideWindow();
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
}
@@ -393,6 +425,40 @@ public sealed partial class MainWindow : Window,
}
}
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
{
if (activatedEventArgs == null)
{
Summon(string.Empty);
return;
}
if (activatedEventArgs.Kind == Microsoft.Windows.AppLifecycle.ExtendedActivationKind.Protocol)
{
if (activatedEventArgs.Data is IProtocolActivatedEventArgs protocolArgs)
{
if (protocolArgs.Uri.ToString() is string uri)
{
// was the URI "x-cmdpal://background" ?
if (uri.StartsWith("x-cmdpal://background", StringComparison.OrdinalIgnoreCase))
{
// we're running, we don't want to activate our window. bail
return;
}
else if (uri.StartsWith("x-cmdpal://settings", StringComparison.OrdinalIgnoreCase))
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
return;
}
}
return;
}
}
Activate();
}
public void Summon(string commandId) =>
// The actual showing and hiding of the window will be done by the
@@ -401,11 +467,6 @@ public sealed partial class MainWindow : Window,
// know till the message is being handled.
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(commandId, _hwnd));
#pragma warning disable SA1310 // Field names should not contain underscore
private const uint DOT_KEY = 0xBE;
private const uint WM_HOTKEY = 0x0312;
#pragma warning restore SA1310 // Field names should not contain underscore
private void UnregisterHotkeys()
{
_keyboardListener.ClearHotkeys();
@@ -476,10 +537,24 @@ public sealed partial class MainWindow : Window,
var isRootHotkey = string.IsNullOrEmpty(commandId);
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
var isVisible = this.Visible;
unsafe
{
// We need to check if our window is cloaked or not. A cloaked window is still
// technically visible, because SHOW/HIDE != iconic (minimized) != cloaked
// (these are all separate states)
long attr = 0;
PInvoke.DwmGetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &attr, sizeof(long));
if (attr == 1 /* DWM_CLOAKED_APP */)
{
isVisible = false;
}
}
// Note to future us: the wParam will have the index of the hotkey we registered.
// We can use that in the future to differentiate the hotkeys we've pressed
// so that we can bind hotkeys to individual commands
if (!this.Visible || !isRootHotkey)
if (!isVisible || !isRootHotkey)
{
Activate();
@@ -487,7 +562,16 @@ public sealed partial class MainWindow : Window,
}
else if (isRootHotkey)
{
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// If there's a debugger attached...
if (System.Diagnostics.Debugger.IsAttached)
{
// ... then manually hide our window. When debugged, we won't get the cool cloaking,
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
return;
}
HideWindow();
}
}
@@ -499,29 +583,25 @@ public sealed partial class MainWindow : Window,
{
switch (uMsg)
{
case WM_HOTKEY:
// Prevent the window from maximizing when double-clicking the title bar area
case PInvoke.WM_NCLBUTTONDBLCLK:
return (LRESULT)IntPtr.Zero;
case PInvoke.WM_HOTKEY:
{
var hotkeyIndex = (int)wParam.Value;
if (hotkeyIndex < _hotkeys.Count)
{
if (_ignoreHotKeyWhenFullScreen)
{
// If we're in full screen mode, ignore the hotkey
if (WindowHelper.IsWindowFullscreen())
{
return (LRESULT)IntPtr.Zero;
}
}
var hotkey = _hotkeys[hotkeyIndex];
HandleSummon(hotkey.CommandId);
// var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
// // Note to future us: the wParam will have the index of the hotkey we registered.
// // We can use that in the future to differentiate the hotkeys we've pressed
// // so that we can bind hotkeys to individual commands
// if (!this.Visible || !isRootHotkey)
// {
// Activate();
// Summon(hotkey.CommandId);
// }
// else if (isRootHotkey)
// {
// PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// }
}
return (LRESULT)IntPtr.Zero;

View File

@@ -66,7 +66,7 @@
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

View File

@@ -25,6 +25,8 @@ SHCreateStreamOnFileEx
CoAllowSetForegroundWindow
SHCreateStreamOnFileEx
SHLoadIndirectString
WM_HOTKEY
WM_NCLBUTTONDBLCLK
Shell_NotifyIcon
LoadIcon
@@ -36,3 +38,8 @@ ExtractIconEx
WM_RBUTTONUP
WM_LBUTTONUP
WM_LBUTTONDBLCLK
MessageBox
DwmGetWindowAttribute
DwmSetWindowAttribute
DWM_CLOAKED_APP

View File

@@ -70,6 +70,13 @@
DisplayName="ms-resource:StartupTaskNameDev" />
</uap5:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="x-cmdpal">
<uap:Logo>Assets\StoreLogo.png</uap:Logo>
<uap:DisplayName>Command Palette Dev URI scheme</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>

View File

@@ -70,6 +70,14 @@
DisplayName="ms-resource:StartupTaskName" />
</uap5:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="x-cmdpal">
<uap:Logo>Assets\StoreLogo.png</uap:Logo>
<uap:DisplayName>Command Palette URI scheme</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>

View File

@@ -356,9 +356,8 @@
<cpcontrols:IconBox
x:Name="HeroImageBorder"
MinWidth="64"
MinHeight="64"
MaxHeight="96"
Width="64"
Margin="16,8,16,16"
HorizontalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}"
@@ -368,8 +367,10 @@
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
FontSize="18"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.Details.Title, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="WrapWholeWords"
Visibility="{x:Bind ViewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" />

View File

@@ -187,8 +187,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(null));
var isMainPage = command is MainListPage;
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
@@ -420,18 +418,20 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
_ = DispatcherQueue.TryEnqueue(() =>
{
// Also hide our details pane about here, if we had one
HideDetails();
if (_settingsWindow == null)
{
_settingsWindow = new SettingsWindow();
}
_settingsWindow.Activate();
OpenSettings();
});
}
public void OpenSettings()
{
if (_settingsWindow == null)
{
_settingsWindow = new SettingsWindow();
}
_settingsWindow.Activate();
}
public void Receive(ShowDetailsMessage message)
{
// TERRIBLE HACK TODO GH #245

View File

@@ -2,10 +2,14 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.CmdPal.UI.Events;
using Microsoft.PowerToys.Telemetry;
using Microsoft.Windows.AppLifecycle;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.UI;
@@ -30,7 +34,33 @@ internal sealed class Program
return 0;
}
Logger.InitializeLogger("\\CmdPal\\Logs\\");
try
{
Logger.InitializeLogger("\\CmdPal\\Logs\\");
}
catch (COMException e)
{
// This is unexpected. For the sake of debugging:
// pop a message box
PInvoke.MessageBox(
(HWND)IntPtr.Zero,
$"Failed to initialize the logger. COMException: \r{e.Message}",
"Command Palette",
MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONERROR);
return 0;
}
catch (Exception e2)
{
// This is unexpected. For the sake of debugging:
// pop a message box
PInvoke.MessageBox(
(HWND)IntPtr.Zero,
$"Failed to initialize the logger. Unknown Exception: \r{e2.Message}",
"Command Palette",
MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONERROR);
return 0;
}
Logger.LogDebug($"Starting at {DateTime.UtcNow}");
PowerToysTelemetry.Log.WriteEvent(new CmdPalProcessStarted());
@@ -79,7 +109,9 @@ internal sealed class Program
if (thisApp.AppWindow is not null and
MainWindow mainWindow)
{
mainWindow.Summon(string.Empty);
mainWindow.HandleLaunch(args);
// mainWindow.Summon(string.Empty);
}
}
}

View File

@@ -2,7 +2,8 @@
"profiles": {
"Microsoft.CmdPal.UI (Package)": {
"commandName": "MsixPackage",
"nativeDebugging": false
"nativeDebugging": false,
"doNotLaunchApp": false
},
"Microsoft.CmdPal.UI (Unpackaged)": {
"commandName": "Project"

View File

@@ -113,7 +113,24 @@
Visibility="{x:Bind ViewModel.HasSettings}" />
<Frame x:Name="SettingsFrame" Visibility="{x:Bind ViewModel.HasSettings}">
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
<controls:SwitchPresenter
HorizontalAlignment="Stretch"
TargetType="x:Boolean"
Value="{x:Bind ViewModel.LoadingSettings, Mode=OneWay}">
<controls:Case Value="True">
<ProgressRing
Width="36"
Height="36"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsIndeterminate="True" />
</controls:Case>
<controls:Case Value="False">
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
</controls:Case>
</controls:SwitchPresenter>
</Frame>
<TextBlock

View File

@@ -48,6 +48,9 @@
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE7FC;}">
<ToggleSwitch IsOn="{x:Bind viewModel.IgnoreShortcutWhenFullscreen, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" />
</controls:SettingsCard>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
<winuiex:WindowEx
x:Class="Microsoft.CmdPal.UI.Settings.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -7,13 +7,18 @@
xmlns:local="using:Microsoft.CmdPal.UI.Settings"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:winuiex="using:WinUIEx"
Title="SettingsWindow"
Width="1280"
Height="720"
MinWidth="480"
MinHeight="480"
Activated="Window_Activated"
Closed="Window_Closed"
mc:Ignorable="d">
<Window.SystemBackdrop>
<winuiex:WindowEx.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
</winuiex:WindowEx.SystemBackdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -37,10 +42,10 @@
Height="16"
Source="ms-appx:///Assets/icon.svg" />
<TextBlock
x:Uid="CmdPalSettingsHeader"
Margin="12,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Command Palette Settings" />
Style="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
<NavigationView
x:Name="NavView"
@@ -100,4 +105,4 @@
</Grid>
</NavigationView>
</Grid>
</Window>
</winuiex:WindowEx>

Some files were not shown because too many files have changed in this diff Show More