Compare commits

...

153 Commits

Author SHA1 Message Date
Mike Griese
5a328c429b it runs again 2026-01-26 15:48:56 -06:00
Mike Griese
41f14c4bf4 Merge remote-tracking branch 'origin/niels9001/cmdpal-dock/drag' into dev/migrie/f/powerdock 2026-01-26 15:30:39 -06:00
Mike Griese
2913477f07 Merge remote-tracking branch 'origin/niels9001/cmdpal-dock/drag' into dev/migrie/f/powerdock 2026-01-26 15:30:30 -06:00
Niels Laute
0cc5c08ea4 Disabling AOT again and fixing it for real this time 2026-01-26 21:57:37 +01:00
Niels Laute
2494bf84b3 unbreaking orientation switching 2026-01-26 21:23:15 +01:00
Niels Laute
7f217609f9 wasted 5 hours of my life but it works 2026-01-26 21:03:53 +01:00
Niels Laute
a0c8b70697 Clean up settings screen 2026-01-26 16:06:00 +01:00
Niels Laute
7833526186 Cleaning up dead code 2026-01-26 15:18:23 +01:00
Niels Laute
b56e4ea560 Tweaks 2026-01-26 13:46:31 +01:00
Niels Laute
80e734587b Styling improvements 2026-01-26 12:23:58 +01:00
Niels Laute
daf586b86d Adding pinned items 2026-01-26 11:36:05 +01:00
Niels Laute
95247aa6d5 Update label position 2026-01-25 15:06:12 +01:00
Niels Laute
947457d20c Add show/hide labels 2026-01-25 14:53:18 +01:00
Niels Laute
5d4a971bcf Adding Center Listview 2026-01-25 13:56:08 +01:00
Niels Laute
0cacfa3cc9 Removing unused resource files 2026-01-25 13:39:19 +01:00
Mike Griese
40f3425a1b xamlformat 2026-01-23 14:12:02 -06:00
Mike Griese
c32f67bc5a oh now I get what that was for 2026-01-23 13:57:02 -06:00
Mike Griese
ba2ad4b317 fewer duplicated items 2026-01-23 13:40:59 -06:00
Mike Griese
d46bb66c11 make sure that the icons show up in the settings 2026-01-23 13:29:27 -06:00
Mike Griese
ea0af2bb9c fix an issue where the SUI wouldnt update the dropdown for sides 2026-01-23 12:54:40 -06:00
Mike Griese
806d9eebe6 cleanup 2026-01-23 12:14:07 -06:00
Mike Griese
e24cf24835 fix the alignment 2026-01-23 10:49:43 -06:00
Mike Griese
30ba1e7aca Merge branch 'dev/migrie/f/powerdock-clanker-theming' into dev/migrie/f/powerdock 2026-01-23 06:20:07 -06:00
Mike Griese
2996d4b9d5 Merge branch 'dev/migrie/f/powerdock' into dev/migrie/f/powerdock-clanker-theming 2026-01-23 06:16:02 -06:00
Mike Griese
efec6cfc03 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2026-01-23 06:13:49 -06:00
Mike Griese
d5017fffe4 Merge branch 'niels9001/cmdpal-dock/drag' into dev/migrie/f/powerdock 2026-01-23 06:12:49 -06:00
Shawn Yuan
086c63b6af [Settings] Fix right click menu display issue (#44982)
<!-- 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
This pull request updates the tray icon context menu logic to better
reflect the state of the "Quick Access" feature. The menu now
dynamically updates its items and labels based on whether Quick Access
is enabled or disabled, improving clarity for users.

**Menu behavior improvements:**

* The tray icon menu now reloads itself when the Quick Access setting
changes, ensuring the menu always matches the current state.
* The "Settings" menu item label changes to "Settings\tLeft-click" when
Quick Access is disabled, providing clearer instructions to users.
[[1]](diffhunk://#diff-e5efbda4c356e159a6ca82a425db84438ab4014d1d90377b98a2eb6d9632d32dR176-R179)
[[2]](diffhunk://#diff-7139ecb2cf76e472c574a155268c19e919e2cce05d9d345c50c1f1bffc939e1aR198-R248)
* The Quick Access menu item is removed from the context menu when the
feature is disabled, preventing confusion.

**Internal state tracking:**

* Added a new variable `last_quick_access_state` to track the previous
Quick Access state and trigger menu reloads only when necessary.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44810
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
- When Quick Access is disabled

<img width="1537" height="312" alt="image"
src="https://github.com/user-attachments/assets/5d51f24e-ccb4-4973-afaa-8b64cc35db87"
/>

- When Quick Access is enabled
<img width="1601" height="201" alt="image"
src="https://github.com/user-attachments/assets/56366d10-bcec-4892-b2d2-f8213ad726aa"
/>

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-23 14:47:35 +08:00
moooyo
d192672c74 fix: Improve Unicode normalization and add regex metachar tests (#44944)
Enhanced SanitizeAndNormalize to handle Unicode normalization more
robustly, ensuring correct buffer sizing and error handling. Added unit
tests for regex metacharacters `$` and `^` to verify correct replacement
behavior at string boundaries. Improves Unicode support and test
coverage for regex edge cases.

<!-- 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

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44942 #44892
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2026-01-23 14:43:21 +08:00
Mike Griese
1bffcfb6fa this settings layout makes more sense 2026-01-22 09:08:51 -06:00
Mike Griese
5d55c5c120 transparent color too 2026-01-21 19:47:05 -06:00
Mike Griese
90df6f15ae RequestedTheme too 2026-01-21 16:35:47 -06:00
Mike Griese
aa6acf8145 the theme sorta works now, but not the text 2026-01-21 16:22:56 -06:00
Mike Griese
3bae9a57e7 this is more correct I think 2026-01-21 16:01:18 -06:00
Mike Griese
e0972996ff I mean, it's doing better than I thought it would 2026-01-21 12:53:52 -06:00
Mike Griese
90afe7e9f5 let the clanker try to add theming 2026-01-21 12:15:07 -06:00
Niels Laute
884d1ec6d6 Seperating converters and making it work on init 2026-01-21 18:30:23 +01:00
Niels Laute
0b38be2c01 Moving Dock setting to Docks page 2026-01-21 18:30:08 +01:00
Niels Laute
5d7211cf85 Making it build again 2026-01-21 17:18:56 +01:00
Niels Laute
a5e93da8ad Disabling AOT 2026-01-21 17:16:54 +01:00
Niels Laute
17aa472af5 Moving the content to its own control 2026-01-21 17:16:42 +01:00
Mike Griese
96581fd24c Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2026-01-21 06:27:42 -06:00
Mike Griese
41d12017b6 code cleanup 2026-01-21 06:27:30 -06:00
Niels Laute
6e7add6feb Adding discard button and moving content to a control 2026-01-19 20:45:48 +01:00
Niels Laute
d55bad1457 ITS WORKING 2026-01-19 15:04:33 +01:00
Niels Laute
e02d384051 Edit mode 2026-01-19 12:56:24 +01:00
Niels Laute
2293b76f7b Dragging works 2026-01-19 11:26:51 +01:00
Mike Griese
b7ea22e017 Merge branch 'dev/migrie/f/powerdock' of https://github.com/microsoft/powertoys into dev/migrie/f/powerdock 2026-01-15 14:38:27 -06:00
Mike Griese
d83dc94841 nits 2026-01-15 14:37:58 -06:00
Mike Griese
031f29418e fix aot? 2026-01-15 13:56:11 -06:00
Niels Laute
fe533cd350 Adding scrollcontainer 2026-01-15 18:04:18 +01:00
Mike Griese
d4802fec40 nits and comments 2026-01-15 06:45:49 -06:00
Mike Griese
12af2770e6 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2026-01-15 06:19:42 -06:00
Mike Griese
d64ccf686f remove unneeded cruft 2026-01-15 06:19:33 -06:00
Mike Griese
17b840c583 nits 2026-01-15 06:09:30 -06:00
Mike Griese
421df9f1e0 fix the wiggle 2026-01-14 16:20:53 -06:00
Mike Griese
806d383272 less jiggling. not no jiggling 2026-01-14 12:49:37 -06:00
Mike Griese
bffce430cc removing buttons 2026-01-14 06:38:55 -06:00
Mike Griese
b310df6e2e cleanup 2026-01-14 06:11:42 -06:00
Mike Griese
1e1f0118d9 minor todo! 2026-01-13 14:33:42 -06:00
Mike Griese
037062e754 fix the clock 2026-01-13 14:20:59 -06:00
Mike Griese
19d77a4d02 loc 2026-01-13 13:58:41 -06:00
Mike Griese
72c0922af7 subtitles? 2026-01-13 13:07:04 -06:00
Mike Griese
329a6434f8 Revert "apparently the flickering is impossible to solve"
This reverts commit b0250fc0f4.
2026-01-13 13:06:56 -06:00
Mike Griese
b0250fc0f4 apparently the flickering is impossible to solve 2026-01-13 13:06:36 -06:00
Mike Griese
029c894ee4 Localized resources, vaguely the way that devhome did 2026-01-13 10:56:08 -06:00
Mike Griese
f624625344 the rest of the widgets are working again 2026-01-13 06:48:34 -06:00
Mike Griese
2c61ec1a8c the devhome graphs work 2026-01-12 16:54:52 -06:00
Mike Griese
3e4e3dd6f1 start porting the devhome code here 2026-01-12 13:28:04 -06:00
Mike Griese
edcce595df Revert all WW changes 2026-01-12 06:36:17 -06:00
Mike Griese
ef0efcbe2f Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2026-01-12 06:27:29 -06:00
Mike Griese
4de34eca96 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2026-01-09 06:52:11 -06:00
Mike Griese
f886d52484 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2026-01-09 06:52:01 -06:00
Mike Griese
017966e3db Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-12-13 11:01:14 -06:00
Mike Griese
e499f90ee5 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-12-11 15:59:07 -06:00
Mike Griese
c0fe992e37 fixes from the upstream merge 2025-12-05 14:14:06 -06:00
Mike Griese
bd316d4d34 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-12-05 13:24:32 -06:00
Mike Griese
f03eb96b9c the more commands too 2025-12-05 13:16:20 -06:00
Mike Griese
bbd15a3ae8 actually respect the setting 2025-12-05 12:39:30 -06:00
Mike Griese
777a301666 default settings 2025-12-05 10:56:41 -06:00
Mike Griese
f9e3ab4852 fix the title on the settings page 2025-12-05 10:02:29 -06:00
Mike Griese
f311a65708 More small refactoring 2025-12-05 08:14:09 -06:00
Mike Griese
b9040d82c3 refactoring to make code 1% more readable 2025-12-05 07:00:48 -06:00
Mike Griese
1d8b45f824 wholesale steal craig's perf monitor 2025-12-04 10:41:28 -06:00
Mike Griese
221cf083bc Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-12-04 09:00:59 -06:00
Mike Griese
ccac1e1ac9 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-12-03 12:52:29 -06:00
Mike Griese
fb428b2d61 update wt.json for building SDK 2025-11-25 16:22:30 -06:00
Mike Griese
acb933643a toolkit updated to support these 2025-11-24 09:02:50 -06:00
Mike Griese
f63785d80d initial spec 2025-11-24 06:25:19 -06:00
Mike Griese
87c1a73ecc bunch of loc todos 2025-11-24 06:24:52 -06:00
Mike Griese
44b0b9ac67 cleanup; tooltips 2025-11-21 15:58:38 -06:00
Mike Griese
7629c6fbfa Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-20 16:49:09 -06:00
Mike Griese
b8c024ac07 remove some dead code 2025-11-20 16:49:01 -06:00
Mike Griese
640c1a8388 pull to separate file 2025-11-20 16:30:46 -06:00
Mike Griese
78b2b23764 I'm guessing that niels will want this 2025-11-20 16:21:24 -06:00
Mike Griese
46d26041b9 don't write the settings when opening it 2025-11-20 16:17:57 -06:00
Mike Griese
08454f8b18 rudimentary settings saving 2025-11-20 15:58:33 -06:00
Mike Griese
b7a65ab609 show num items in band 2025-11-20 15:24:42 -06:00
Mike Griese
08d3435a0d Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-20 05:27:46 -06:00
Mike Griese
46b8eea695 more settings for labels 2025-11-18 11:06:01 -06:00
Mike Griese
5b255011c7 We need to have a separate helper for "wrap as band" vs "this is a pinned thing" 2025-11-18 10:48:21 -06:00
Mike Griese
6782829cdd icons are nice 2025-11-18 09:40:40 -06:00
Mike Griese
6ed8d73b50 Make it a combobox 2025-11-18 09:25:08 -06:00
Mike Griese
38dfee0234 This works! 2025-11-18 06:59:51 -06:00
Mike Griese
d547a6f613 Start plumbing the settings for bands through 2025-11-18 06:02:07 -06:00
Mike Griese
58bea1c813 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-17 16:02:12 -06:00
Mike Griese
5ad2bdf6c2 xaml 2025-11-09 09:13:10 -06:00
Mike Griese
44f739a289 that's better 2025-11-09 08:28:25 -06:00
Mike Griese
f3d9fc2342 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-09 07:02:00 -06:00
Mike Griese
90d4ca060e don't show the secondary command if its a separator 2025-11-09 07:01:52 -06:00
Mike Griese
6554a4aaaa add a 'customize' item to the context menu of the bar itself 2025-11-09 06:56:06 -06:00
Mike Griese
cac0048ca7 we start now boys 2025-11-08 06:07:55 -06:00
Mike Griese
ddb28a8606 Pin & Unpin, and fix a closing crash 2025-11-07 14:44:14 -06:00
Mike Griese
a7206863bc gimme dis\nnow 2025-11-07 13:53:35 -06:00
Mike Griese
96def3b79a much simpler settings 2025-11-07 13:19:36 -06:00
Mike Griese
5231543ed2 Rudimentary: This _does_ actually pin commands to the dock 2025-11-07 13:03:47 -06:00
Mike Griese
2462da68bc some nits around the sizing, to make things with subtitles show again 2025-11-07 06:29:46 -06:00
Mike Griese
bbfa6c6ccb hell yea it works 2025-11-07 06:12:48 -06:00
Mike Griese
f0ea908ee6 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-06 15:27:49 -06:00
Mike Griese
6e11230fed Merge branch 'dev/migrie/f/powerdock' of https://github.com/microsoft/powertoys into dev/migrie/f/powerdock 2025-11-06 15:24:10 -06:00
Mike Griese
6c26e86e9a dead end trying to pin new top-level commands 2025-11-06 15:23:56 -06:00
Mike Griese
1d19705568 notes 2025-11-02 06:50:09 -06:00
Mike Griese
e5e20eca9c fix the showdesktop handler 2025-11-02 06:40:35 -06:00
Niels Laute
ef0639602f Minor padding tweaks on left/right 2025-11-01 19:09:45 +01:00
Niels Laute
fdd4416049 Adding design polish and make it look like taskbar (border and button styles) 2025-11-01 19:09:13 +01:00
Mike Griese
0dab46e58f tighten up opening cmdpal in a couple places 2025-10-31 15:03:46 -05:00
Mike Griese
86d1061a25 Yep, open the palette right at the dock 2025-10-31 14:40:31 -05:00
Mike Griese
e0197dd7a5 holy fuck this is cool 2025-10-31 12:55:25 -05:00
Mike Griese
64ea63b77d handle esc to dismiss dock flyout 2025-10-31 12:06:45 -05:00
Mike Griese
bc6b2af03c Add context menus to buttons 2025-10-31 06:10:41 -05:00
Mike Griese
c1af5fdc57 meh settings for icon sizes 2025-10-30 16:56:32 -05:00
Mike Griese
5be208520e update the clock in RT ; accidentally regress clipboard name 2025-10-28 16:12:54 -05:00
Mike Griese
5aaf0e010a add a dedicated VM for DockBandItems 2025-10-28 05:55:57 -05:00
Mike Griese
48eee1b0d9 do less work on the UI thread 2025-10-27 15:43:17 -05:00
Mike Griese
1447a825ee right right, update the observable thing on the UI thread dumbass 2025-10-27 15:35:10 -05:00
Mike Griese
76f7dd3b09 don't flicker the windows so much bub 2025-10-27 10:57:09 -05:00
Mike Griese
ee174ddd1d bands come from the settings 2025-10-27 10:25:24 -05:00
Mike Griese
35c4f8fdaa add my deskband 2025-10-27 09:57:00 -05:00
Mike Griese
2ec7ae664e actually hot-reload dock settings 2025-10-27 06:25:29 -05:00
Mike Griese
1b8ddaa849 add the settings to the main settings, even if they do nothing 2025-10-27 05:59:15 -05:00
Mike Griese
d6bca1d38e different sides? yes pls 2025-10-26 21:43:27 -05:00
Mike Griese
b1d7626ab7 naming for that weird clipboard command 2025-10-26 20:40:34 -05:00
Mike Griese
91598c091e Have a separate provider for dock bands 2025-10-25 10:45:14 -05:00
Mike Griese
fd3e73ee7e listen for window updates 2025-10-24 12:16:54 -05:00
Mike Griese
06a664a53a Couple things
* hide subtitles on WW items;
* Add a clock band
* discover that items need .Names on the commands to appear and that's gross
2025-10-24 06:56:35 -05:00
Mike Griese
87d2509380 a bunch of WW refactoring 2025-10-24 06:17:35 -05:00
Mike Griese
c1dc487f2c Now all the items are initialized straight off of the commands in cmdpal 2025-10-24 05:37:34 -05:00
Mike Griese
e0dd7ad44a Move out a couple of these files 2025-10-23 15:35:16 -05:00
Mike Griese
aaa68fa351 look ma, those are from CmdPal 2025-10-23 15:32:39 -05:00
Mike Griese
d9e4133b5a Revert "probably don't need any of this"
This reverts commit 821b99c4e0.
2025-10-23 14:51:19 -05:00
Mike Griese
821b99c4e0 probably don't need any of this 2025-10-23 14:51:15 -05:00
Mike Griese
8b5a2e9537 need all this 2025-10-23 14:51:01 -05:00
Mike Griese
2e49835b4d didn't need a different project, really 2025-10-23 05:52:22 -05:00
Mike Griese
ef106f6811 stand up project 2025-10-23 05:51:10 -05:00
118 changed files with 10376 additions and 151 deletions

View File

@@ -886,6 +886,7 @@ Ldr
LEFTALIGN
LEFTSCROLLBAR
LEFTTEXT
leftclick
LError
LEVELID
LExit
@@ -1011,6 +1012,7 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metacharacter
metadatamatters
Metadatas
metafile

View File

@@ -218,6 +218,10 @@
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

View File

@@ -26,6 +26,11 @@
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
"name": "Update template project",
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory."
},
{
"input": " .\\extensionsdk\\nuget\\BuildSDKHelper.ps1 -VersionOfSDK 0.0.1",
"name": "Build SDK",
"description": "Builds the SDK nuget package with the specified version."
}
]
}

View File

@@ -2,8 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.CmdPal.Core.Common;
public static class CoreLogger
@@ -15,6 +13,8 @@ public static class CoreLogger
private static ILogger? _logger;
public static ILogger? Instance => _logger;
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.Common.Helpers;
public partial class PinnedDockItem : WrappedDockItem
{
public override string Title => $"{base.Title} ({Properties.Resources.PinnedItemSuffix})";
public PinnedDockItem(ICommand command)
: base(command, command.Name)
{
}
public PinnedDockItem(IListItem item, string id)
: base([item], id, item.Title)
{
Icon = item.Icon;
}
}

View File

@@ -9,4 +9,19 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Core.Common.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.Common.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Pinned.
/// </summary>
public static string PinnedItemSuffix {
get {
return ResourceManager.GetString("PinnedItemSuffix", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PinnedItemSuffix" xml:space="preserve">
<value>Pinned</value>
<comment>Suffix shown for pinned items in the dock</comment>
</data>
</root>

View File

@@ -96,9 +96,10 @@ public partial class CommandBarViewModel : ObservableObject,
SecondaryCommand = SelectedItem.SecondaryCommand;
ShouldShowContextMenu = SelectedItem.MoreCommands
.OfType<CommandContextItemViewModel>()
.Count() > 1;
var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
OnPropertyChanged(nameof(HasSecondaryCommand));
OnPropertyChanged(nameof(SecondaryCommand));

View File

@@ -42,7 +42,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
private string _itemTitle = string.Empty;
public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
protected string ItemTitle => _itemTitle;
public virtual string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
public string Subtitle { get; private set; } = string.Empty;
@@ -64,10 +66,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public CommandItemViewModel? PrimaryCommand => this;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
public CommandItemViewModel? SecondaryCommand
{
get
{
if (HasMoreCommands)
{
if (MoreCommands[0] is CommandContextItemViewModel command)
{
return command;
}
}
return null;
}
}
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
public bool HasTitle => !string.IsNullOrEmpty(Title);
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
public virtual bool HasText => HasTitle || HasSubtitle;
public DataPackageView? DataPackage { get; private set; }
public List<IContextItemViewModel> AllCommands
@@ -331,16 +353,19 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Icon));
UpdateProperty(nameof(HasText));
break;
case nameof(Title):
_itemTitle = model.Title;
UpdateProperty(nameof(HasText));
break;
case nameof(Subtitle):
var modelSubtitle = model.Subtitle;
this.Subtitle = modelSubtitle;
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
UpdateProperty(nameof(HasText));
break;
case nameof(Icon):
@@ -427,11 +452,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
}
private void UpdateDefaultContextItemIcon()
{
private void UpdateDefaultContextItemIcon() =>
// Command icon takes precedence over our icon on the primary command
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
}
private void UpdateTitle(string? title)
{

View File

@@ -53,11 +53,8 @@ public partial class ContextMenuViewModel : ObservableObject,
{
if (SelectedItem is not null)
{
if (SelectedItem.MoreCommands.Count() > 1)
{
ContextMenuStack.Clear();
PushContextStack(SelectedItem.AllCommands);
}
ContextMenuStack.Clear();
PushContextStack(SelectedItem.AllCommands);
}
}

View File

@@ -17,7 +17,7 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject
PageContext = new(realContext);
}
internal ExtensionObjectViewModel(WeakReference<IPageContext> context)
protected ExtensionObjectViewModel(WeakReference<IPageContext> context)
{
PageContext = context;
}

View File

@@ -12,6 +12,7 @@ public partial class LoadingPageViewModel : PageViewModel
: base(model, scheduler, host)
{
ModelIsLoading = true;
HasBackButton = false;
IsInitialized = false;
}
}

View File

@@ -4,4 +4,4 @@
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken, bool TransientPage = false);

View File

@@ -18,6 +18,8 @@ public record PerformCommandMessage
public bool WithAnimation { get; set; } = true;
public bool TransientPage { get; set; }
public PerformCommandMessage(ExtensionObject<ICommand> command)
{
Command = command;

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
public sealed record WindowHiddenMessage();

View File

@@ -27,7 +27,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
public partial string ErrorMessage { get; protected set; } = string.Empty;
[ObservableProperty]
public partial bool IsNested { get; set; } = true;
public partial bool IsRootPage { get; set; } = true;
[ObservableProperty]
public partial bool HasBackButton { get; set; } = true;
// This is set from the SearchBar
[ObservableProperty]

View File

@@ -121,4 +121,8 @@
<value>Show details</value>
<comment>Name for the command that shows details of an item</comment>
</data>
<data name="PinnedItemSuffix" xml:space="preserve">
<value>Pinned</value>
<comment>Suffix shown for pinned items in the dock</comment>
</data>
</root>

View File

@@ -16,7 +16,8 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ShellViewModel : ObservableObject,
IDisposable,
IRecipient<PerformCommandMessage>,
IRecipient<HandleCommandResultMessage>
IRecipient<HandleCommandResultMessage>,
IRecipient<WindowHiddenMessage>
{
private readonly IRootPageService _rootPageService;
private readonly IAppHostService _appHostService;
@@ -79,8 +80,9 @@ public partial class ShellViewModel : ObservableObject,
private IPage? _rootPage;
private bool _isNested;
private bool _currentlyTransient;
public bool IsNested => _isNested;
public bool IsNested => _isNested && !_currentlyTransient;
public PageViewModel NullPage { get; private set; }
@@ -96,11 +98,13 @@ public partial class ShellViewModel : ObservableObject,
_appHostService = appHostService;
NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
NullPage.HasBackButton = false;
_currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
// Register to receive messages
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this);
WeakReferenceMessenger.Default.Register<WindowHiddenMessage>(this);
}
[RelayCommand]
@@ -259,7 +263,7 @@ public partial class ShellViewModel : ObservableObject,
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
_rootPageService.OnPerformCommand(message.Context, CurrentPage.IsRootPage, host);
try
{
@@ -269,6 +273,7 @@ public partial class ShellViewModel : ObservableObject,
var isMainPage = command == _rootPage;
_isNested = !isMainPage;
_currentlyTransient = message.TransientPage;
// Telemetry: Track extension page navigation for session metrics
if (host is not null)
@@ -288,6 +293,9 @@ public partial class ShellViewModel : ObservableObject,
throw new NotSupportedException();
}
pageViewModel.IsRootPage = isMainPage;
pageViewModel.HasBackButton = IsNested;
// Clear command bar, ViewModel initialization can already set new commands if it wants to
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
@@ -307,7 +315,8 @@ public partial class ShellViewModel : ObservableObject,
_scheduler);
// While we're loading in the background, immediately move to the next page.
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
NavigateToPageMessage msg = new(pageViewModel, message.WithAnimation, navigationToken, message.TransientPage);
WeakReferenceMessenger.Default.Send(msg);
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
// See RootFrame_Navigated event handler.
@@ -478,6 +487,19 @@ public partial class ShellViewModel : ObservableObject,
UnsafeHandleCommandResult(message.Result.Unsafe);
}
public void Receive(WindowHiddenMessage message)
{
// If the window was hidden while we had a transient page, we need to reset that state.
if (_currentlyTransient)
{
_currentlyTransient = false;
// navigate back to the main page without animation
GoHome(withAnimation: false, focusSearch: false);
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
}
}
private void OnUIThread(Action action)
{
_ = Task.Factory.StartNew(

View File

@@ -18,7 +18,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
{
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
internal static readonly ObservableCollection<Color> WindowsColorSwatches = [
// row 0
Color.FromArgb(255, 255, 185, 0), // #ffb900

View File

@@ -21,7 +21,7 @@ public class CommandPalettePageViewModelFactory
{
return page switch
{
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested },
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsRootPage = !nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host),
_ => null,
};

View File

@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;
using Windows.Foundation;
@@ -27,6 +29,8 @@ public sealed class CommandProviderWrapper
public TopLevelViewModel[] FallbackItems { get; private set; } = [];
public TopLevelViewModel[] DockBandItems { get; private set; } = [];
public string DisplayName { get; private set; } = string.Empty;
public IExtensionWrapper? Extension { get; }
@@ -141,6 +145,7 @@ public sealed class CommandProviderWrapper
ICommandItem[]? commands = null;
IFallbackCommandItem[]? fallbacks = null;
ICommandItem[] dockBands = []; // do not initialize me to null
try
{
@@ -158,6 +163,16 @@ public sealed class CommandProviderWrapper
UnsafePreCacheApiAdditions(two);
}
if (model is ICommandProvider3 supportsDockBands)
{
var bands = supportsDockBands.GetDockBands();
if (bands is not null)
{
CoreLogger.LogDebug($"Found {bands.Length} bands on {DisplayName} ({ProviderId}) ");
dockBands = bands;
}
}
Id = model.Id;
DisplayName = model.DisplayName;
Icon = new(model.Icon);
@@ -168,7 +183,8 @@ public sealed class CommandProviderWrapper
Settings = new(model.Settings, this, _taskScheduler);
// We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
var objects = new TopLevelObjects(commands, fallbacks, dockBands);
InitializeCommands(objects, serviceProvider, pageContext);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
}
@@ -180,33 +196,68 @@ public sealed class CommandProviderWrapper
}
}
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
private record TopLevelObjects(
ICommandItem[]? Commands,
IFallbackCommandItem[]? Fallbacks,
ICommandItem[]? DockBands);
private void InitializeCommands(
TopLevelObjects objects,
IServiceProvider serviceProvider,
WeakReference<IPageContext> pageContext)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var state = serviceProvider.GetService<AppStateModel>()!;
var providerSettings = GetProviderSettings(settings);
Func<ICommandItem?, bool, TopLevelViewModel> makeAndAdd = (ICommandItem? i, bool fallback) =>
Func<ICommandItem?, TopLevelType, TopLevelViewModel> make = (ICommandItem? i, TopLevelType t) =>
{
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
topLevelViewModel.InitializeProperties();
return topLevelViewModel;
};
if (commands is not null)
if (objects.Commands is not null)
{
TopLevelItems = commands
.Select(c => makeAndAdd(c, false))
TopLevelItems = objects.Commands
.Select(c => make(c, TopLevelType.Normal))
.ToArray();
}
if (fallbacks is not null)
if (objects.Fallbacks is not null)
{
FallbackItems = fallbacks
.Select(c => makeAndAdd(c, true))
FallbackItems = objects.Fallbacks
.Select(c => make(c, TopLevelType.Fallback))
.ToArray();
}
if (objects.DockBands is not null)
{
List<TopLevelViewModel> bands = new();
foreach (var b in objects.DockBands)
{
var bandVm = make(b, TopLevelType.DockBand);
bands.Add(bandVm);
}
foreach (var c in TopLevelItems)
{
foreach (var pinnedId in settings.DockSettings.PinnedCommands)
{
if (pinnedId == c.Id)
{
var bandModel = c.ToPinnedDockBandItem();
var bandVm = make(bandModel, TopLevelType.DockBand);
bands.Add(bandVm);
break;
}
}
}
DockBandItems = bands.ToArray();
}
}
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
@@ -219,6 +270,10 @@ public sealed class CommandProviderWrapper
{
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
}
else if (a is ICommandItem[] commands)
{
Logger.LogDebug($"{ProviderId}: Found an ICommandItem[]");
}
}
}
@@ -235,4 +290,14 @@ public sealed class CommandProviderWrapper
// In handling this, a call will be made to `LoadTopLevelCommands` to
// retrieve the new items.
this.CommandsChanged?.Invoke(this, args);
internal void PinDockBand(TopLevelViewModel bandVm)
{
Logger.LogDebug($"CommandProviderWrapper.PinDockBand: {ProviderId} - {bandVm.Id}");
var bands = this.DockBandItems.ToList();
bands.Add(bandVm);
this.DockBandItems = bands.ToArray();
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs());
}
}

View File

@@ -1,9 +1,12 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
@@ -18,6 +21,8 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
private readonly FallbackLogItem _fallbackLogItem = new();
private readonly NewExtensionPage _newExtension = new();
private readonly IRootPageService _rootPageService;
public override ICommandItem[] TopLevelCommands() =>
[
new CommandItem(openSettings) { },
@@ -37,11 +42,22 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
_fallbackLogItem,
];
public BuiltInsCommandProvider()
public BuiltInsCommandProvider(IRootPageService rootPageService)
{
Id = "com.microsoft.cmdpal.builtin.core";
DisplayName = Properties.Resources.builtin_display_name;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
_rootPageService = rootPageService;
}
public override ICommandItem[]? GetDockBands()
{
var rootPage = _rootPageService.GetRootPage();
List<ICommandItem> bandItems = new();
bandItems.Add(new WrappedDockItem(rootPage, Properties.Resources.builtin_command_palette_title));
return bandItems.ToArray();
}
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);

View File

@@ -50,6 +50,7 @@ public partial class MainListPage : DynamicListPage,
public MainListPage(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
{
Id = "com.microsoft.cmdpal.home";
Title = Resources.builtin_home_name;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -19,7 +19,7 @@ public partial class OpenSettingsCommand : InvokableCommand
public override ICommandResult Invoke()
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
return CommandResult.KeepOpen();
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -70,6 +70,15 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
StateJson = model.StateJson;
DataJson = model.DataJson;
RenderCard();
UpdateProperty(nameof(Card));
model.PropChanged += Model_PropChanged;
}
private void RenderCard()
{
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
{
Card = builtCard;
@@ -93,8 +102,41 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
UpdateProperty(nameof(Card));
return;
}
}
UpdateProperty(nameof(Card));
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
FetchProperty(args.PropertyName);
}
catch (Exception ex)
{
ShowException(ex);
}
}
protected virtual void FetchProperty(string propertyName)
{
var model = this._formModel.Unsafe;
if (model is null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(DataJson):
DataJson = model.DataJson;
RenderCard();
break;
case nameof(TemplateJson):
TemplateJson = model.TemplateJson;
RenderCard();
break;
}
UpdateProperty(propertyName);
}
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]

View File

@@ -0,0 +1,255 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Settings;
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
#pragma warning disable SA1402 // File may only contain a single type
public partial class DockBandSettingsViewModel : ObservableObject
{
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
private readonly SettingsModel _settingsModel;
private readonly DockBandSettings _dockSettingsModel;
private readonly TopLevelViewModel _adapter;
private readonly DockBandViewModel? _bandViewModel;
public string Title => _adapter.Title;
public string Description
{
get
{
List<string> parts = [_adapter.ExtensionName];
// Add the number of items in the band
var itemCount = NumItemsInBand();
if (itemCount > 0)
{
var itemsString = itemCount == 1 ?
Properties.Resources.dock_item_count_singular :
string.Format(CultureInfo.CurrentCulture, PluralItemsFormatString, itemCount);
parts.Add(itemsString);
}
return string.Join(" - ", parts);
}
}
public string ProviderId => _adapter.CommandProviderId;
public IconInfoViewModel Icon => _adapter.IconViewModel;
private ShowLabelsOption _showLabels;
public ShowLabelsOption ShowLabels
{
get => _showLabels;
set
{
if (value != _showLabels)
{
_showLabels = value;
_dockSettingsModel.ShowLabels = value switch
{
ShowLabelsOption.Default => null,
ShowLabelsOption.ShowLabels => true,
ShowLabelsOption.HideLabels => false,
_ => null,
};
Save();
}
}
}
private ShowLabelsOption FetchShowLabels()
{
if (_dockSettingsModel.ShowLabels == null)
{
return ShowLabelsOption.Default;
}
return _dockSettingsModel.ShowLabels.Value ? ShowLabelsOption.ShowLabels : ShowLabelsOption.HideLabels;
}
// used to map to ComboBox selection
public int ShowLabelsIndex
{
get => (int)ShowLabels;
set => ShowLabels = (ShowLabelsOption)value;
}
private DockPinSide PinSide
{
get => _pinSide;
set
{
if (value != _pinSide)
{
UpdatePinSide(value);
}
}
}
private DockPinSide _pinSide;
public int PinSideIndex
{
get => (int)PinSide;
set => PinSide = (DockPinSide)value;
}
/// <summary>
/// Gets or sets a value indicating whether the band is pinned to the dock.
/// When enabled, pins to Center. When disabled, removes from all sides.
/// </summary>
public bool IsPinned
{
get => PinSide != DockPinSide.None;
set
{
if (value && PinSide == DockPinSide.None)
{
// Pin to Center by default when enabling
PinSide = DockPinSide.Center;
}
else if (!value && PinSide != DockPinSide.None)
{
// Remove from dock when disabling
PinSide = DockPinSide.None;
}
}
}
public DockBandSettingsViewModel(
DockBandSettings dockSettingsModel,
TopLevelViewModel topLevelAdapter,
DockBandViewModel? bandViewModel,
SettingsModel settingsModel)
{
_dockSettingsModel = dockSettingsModel;
_adapter = topLevelAdapter;
_bandViewModel = bandViewModel;
_settingsModel = settingsModel;
_pinSide = FetchPinSide();
_showLabels = FetchShowLabels();
}
private DockPinSide FetchPinSide()
{
var dockSettings = _settingsModel.DockSettings;
var inStart = dockSettings.StartBands.Any(b => b.Id == _dockSettingsModel.Id);
if (inStart)
{
return DockPinSide.Start;
}
var inCenter = dockSettings.CenterBands.Any(b => b.Id == _dockSettingsModel.Id);
if (inCenter)
{
return DockPinSide.Center;
}
var inEnd = dockSettings.EndBands.Any(b => b.Id == _dockSettingsModel.Id);
if (inEnd)
{
return DockPinSide.End;
}
return DockPinSide.None;
}
private int NumItemsInBand()
{
var bandVm = _bandViewModel;
if (bandVm is null)
{
return 0;
}
return _bandViewModel!.Items.Count;
}
private void Save()
{
SettingsModel.SaveSettings(_settingsModel);
}
private void UpdatePinSide(DockPinSide value)
{
OnPinSideChanged(value);
OnPropertyChanged(nameof(PinSideIndex));
OnPropertyChanged(nameof(PinSide));
OnPropertyChanged(nameof(IsPinned));
}
public void SetBandPosition(DockPinSide side, int? index)
{
var dockSettings = _settingsModel.DockSettings;
// Remove from all sides first
dockSettings.StartBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
dockSettings.CenterBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
dockSettings.EndBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
// Add to the selected side
switch (side)
{
case DockPinSide.Start:
{
var insertIndex = index ?? dockSettings.StartBands.Count;
dockSettings.StartBands.Insert(insertIndex, _dockSettingsModel);
break;
}
case DockPinSide.Center:
{
var insertIndex = index ?? dockSettings.CenterBands.Count;
dockSettings.CenterBands.Insert(insertIndex, _dockSettingsModel);
break;
}
case DockPinSide.End:
{
var insertIndex = index ?? dockSettings.EndBands.Count;
dockSettings.EndBands.Insert(insertIndex, _dockSettingsModel);
break;
}
case DockPinSide.None:
default:
// Do nothing
break;
}
Save();
}
private void OnPinSideChanged(DockPinSide value)
{
SetBandPosition(value, null);
_pinSide = value;
}
}
public enum DockPinSide
{
None,
Start,
Center,
End,
}
public enum ShowLabelsOption
{
Default,
ShowLabels,
HideLabels,
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,196 @@
// 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 Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
#pragma warning disable SA1402 // File may only contain a single type
public sealed partial class DockBandViewModel : ExtensionObjectViewModel
{
private readonly CommandItemViewModel _rootItem;
private readonly DockBandSettings _bandSettings;
private readonly DockSettings _dockSettings;
private readonly Action _saveSettings;
public ObservableCollection<DockItemViewModel> Items { get; } = new();
private bool _showLabels = true;
private bool? _showLabelsSnapshot;
public string Id => _rootItem.Command.Id;
/// <summary>
/// Gets or sets a value indicating whether labels are shown for items in this band.
/// This is a preview value - call <see cref="SaveShowLabels"/> to persist or
/// <see cref="RestoreShowLabels"/> to discard changes.
/// </summary>
public bool ShowLabels
{
get => _showLabels;
set
{
if (_showLabels != value)
{
_showLabels = value;
foreach (var item in Items)
{
item.ShowLabel = value;
}
}
}
}
/// <summary>
/// Takes a snapshot of the current ShowLabels value before editing.
/// </summary>
internal void SnapshotShowLabels()
{
_showLabelsSnapshot = _showLabels;
}
/// <summary>
/// Saves the current ShowLabels value to settings.
/// </summary>
internal void SaveShowLabels()
{
_bandSettings.ShowLabels = _showLabels;
_showLabelsSnapshot = null;
}
/// <summary>
/// Restores the ShowLabels value from the snapshot.
/// </summary>
internal void RestoreShowLabels()
{
if (_showLabelsSnapshot.HasValue)
{
ShowLabels = _showLabelsSnapshot.Value;
_showLabelsSnapshot = null;
}
}
internal DockBandViewModel(
CommandItemViewModel commandItemViewModel,
WeakReference<IPageContext> errorContext,
DockBandSettings settings,
DockSettings dockSettings,
Action saveSettings)
: base(errorContext)
{
_rootItem = commandItemViewModel;
_bandSettings = settings;
_dockSettings = dockSettings;
_saveSettings = saveSettings;
_showLabels = settings.ResolveShowLabels(dockSettings.ShowLabels);
}
private void InitializeFromList(IListPage list)
{
var items = list.GetItems();
var newViewModels = new List<DockItemViewModel>();
foreach (var item in items)
{
var newItemVm = new DockItemViewModel(new(item), this.PageContext, _showLabels);
newItemVm.SlowInitializeProperties();
newViewModels.Add(newItemVm);
}
DoOnUiThread(() =>
{
ListHelpers.InPlaceUpdateList(Items, newViewModels, out var removed);
});
// TODO! dispose removed VMs
}
public override void InitializeProperties()
{
var command = _rootItem.Command;
var list = command.Model.Unsafe as IListPage;
if (list is not null)
{
InitializeFromList(list);
list.ItemsChanged += HandleItemsChanged;
}
else
{
DoOnUiThread(() =>
{
var dockItem = new DockItemViewModel(_rootItem, _showLabels);
dockItem.SlowInitializeProperties();
Items.Add(dockItem);
});
}
}
private void HandleItemsChanged(object sender, IItemsChangedEventArgs args)
{
if (_rootItem.Command.Model.Unsafe is IListPage p)
{
InitializeFromList(p);
}
}
}
public partial class DockItemViewModel : CommandItemViewModel
{
private bool _showLabel = true;
public bool ShowLabel
{
get => _showLabel;
internal set
{
if (_showLabel != value)
{
_showLabel = value;
UpdateProperty(nameof(ShowLabel));
UpdateProperty(nameof(HasText));
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Subtitle));
}
}
}
public override string Title => _showLabel ? ItemTitle : string.Empty;
public new string Subtitle => _showLabel ? base.Subtitle : string.Empty;
public override bool HasText => _showLabel ? base.HasText : false;
/// <summary>
/// Gets the tooltip for the dock item, which includes the title and
/// subtitle. If it doesn't have one part, it just returns the other.
/// </summary>
/// <remarks>
/// Trickery: in the case one is empty, we can just concatenate, and it will
/// always only be the one that's non-empty
/// </remarks>
public string Tooltip =>
!string.IsNullOrEmpty(Title) && !string.IsNullOrEmpty(Subtitle) ?
$"{Title}\n{Subtitle}" :
Title + Subtitle;
public DockItemViewModel(CommandItemViewModel root, bool showLabel)
: this(root.Model, root.PageContext, showLabel)
{
}
public DockItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext, bool showLabel)
: base(item, errorContext)
{
_showLabel = showLabel;
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,602 @@
// 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.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
public sealed partial class DockViewModel : IDisposable,
IRecipient<CommandsReloadedMessage>,
IPageContext
{
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly SettingsModel _settingsModel;
private DockSettings _settings;
public TaskScheduler Scheduler { get; }
public ObservableCollection<DockBandViewModel> StartItems { get; } = new();
public ObservableCollection<DockBandViewModel> CenterItems { get; } = new();
public ObservableCollection<DockBandViewModel> EndItems { get; } = new();
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
public DockViewModel(
TopLevelCommandManager tlcManager,
SettingsModel settings,
TaskScheduler scheduler)
{
_topLevelCommandManager = tlcManager;
_settingsModel = settings;
_settings = settings.DockSettings;
Scheduler = scheduler;
WeakReferenceMessenger.Default.Register<CommandsReloadedMessage>(this);
}
public void UpdateSettings(DockSettings settings)
{
Logger.LogDebug($"DockViewModel.UpdateSettings");
_settings = settings;
SetupBands();
}
private void SetupBands()
{
Logger.LogDebug($"Setting up dock bands");
SetupBands(_settings.StartBands, StartItems);
SetupBands(_settings.CenterBands, CenterItems);
SetupBands(_settings.EndBands, EndItems);
}
private void SetupBands(
List<DockBandSettings> bands,
ObservableCollection<DockBandViewModel> target)
{
List<DockBandViewModel> newBands = new();
foreach (var band in bands)
{
var commandId = band.Id;
var topLevelCommand = _topLevelCommandManager.LookupDockBand(commandId);
if (topLevelCommand is null)
{
Logger.LogWarning($"Failed to find band {commandId}");
}
if (topLevelCommand is not null)
{
var bandVm = CreateBandItem(band, topLevelCommand.ItemViewModel);
newBands.Add(bandVm);
}
}
var beforeCount = target.Count;
var afterCount = newBands.Count;
DoOnUiThread(() =>
{
ListHelpers.InPlaceUpdateList(target, newBands, out var removed);
var isStartBand = target == StartItems;
var label = isStartBand ? "Start bands:" : "End bands:";
Logger.LogDebug($"{label} ({beforeCount}) -> ({afterCount}), Removed {removed?.Count ?? 0} items");
});
}
public void Dispose()
{
}
public void Receive(CommandsReloadedMessage message)
{
SetupBands();
CoreLogger.LogDebug("Bands reloaded");
}
private DockBandViewModel CreateBandItem(
DockBandSettings bandSettings,
CommandItemViewModel commandItem)
{
DockBandViewModel band = new(commandItem, new(this), bandSettings, _settings, SaveSettings);
band.InitializeProperties(); // TODO! make async
return band;
}
private void SaveSettings()
{
SettingsModel.SaveSettings(_settingsModel);
}
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
{
var id = tlc.Id;
return FindBandById(id);
}
public DockBandViewModel? FindBandById(string id)
{
foreach (var band in StartItems)
{
if (band.Id == id)
{
return band;
}
}
foreach (var band in CenterItems)
{
if (band.Id == id)
{
return band;
}
}
foreach (var band in EndItems)
{
if (band.Id == id)
{
return band;
}
}
return null;
}
/// <summary>
/// Syncs the band position in settings after a same-list reorder.
/// Does not save to disk - call SaveBandOrder() when done editing.
/// </summary>
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
{
var bandId = band.Id;
var dockSettings = _settingsModel.DockSettings;
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
?? dockSettings.CenterBands.FirstOrDefault(b => b.Id == bandId)
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
if (bandSettings == null)
{
return;
}
// Remove from all settings lists
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
// Add to target settings list at the correct index
var targetSettings = targetSide switch
{
DockPinSide.Start => dockSettings.StartBands,
DockPinSide.Center => dockSettings.CenterBands,
DockPinSide.End => dockSettings.EndBands,
_ => dockSettings.StartBands,
};
var insertIndex = Math.Min(targetIndex, targetSettings.Count);
targetSettings.Insert(insertIndex, bandSettings);
}
/// <summary>
/// Moves a dock band to a new position (cross-list drop).
/// Does not save to disk - call SaveBandOrder() when done editing.
/// </summary>
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
{
var bandId = band.Id;
var dockSettings = _settingsModel.DockSettings;
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
?? dockSettings.CenterBands.FirstOrDefault(b => b.Id == bandId)
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
if (bandSettings == null)
{
Logger.LogWarning($"Could not find band settings for band {bandId}");
return;
}
// Remove from all sides (settings and UI)
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
StartItems.Remove(band);
CenterItems.Remove(band);
EndItems.Remove(band);
// Add to the target side at the specified index
switch (targetSide)
{
case DockPinSide.Start:
{
var settingsIndex = Math.Min(targetIndex, dockSettings.StartBands.Count);
dockSettings.StartBands.Insert(settingsIndex, bandSettings);
var uiIndex = Math.Min(targetIndex, StartItems.Count);
StartItems.Insert(uiIndex, band);
break;
}
case DockPinSide.Center:
{
var settingsIndex = Math.Min(targetIndex, dockSettings.CenterBands.Count);
dockSettings.CenterBands.Insert(settingsIndex, bandSettings);
var uiIndex = Math.Min(targetIndex, CenterItems.Count);
CenterItems.Insert(uiIndex, band);
break;
}
case DockPinSide.End:
{
var settingsIndex = Math.Min(targetIndex, dockSettings.EndBands.Count);
dockSettings.EndBands.Insert(settingsIndex, bandSettings);
var uiIndex = Math.Min(targetIndex, EndItems.Count);
EndItems.Insert(uiIndex, band);
break;
}
}
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
}
/// <summary>
/// Saves the current band order and label settings to settings.
/// Call this when exiting edit mode.
/// </summary>
public void SaveBandOrder()
{
// Save ShowLabels for all bands
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
{
band.SaveShowLabels();
}
_snapshotStartBands = null;
_snapshotCenterBands = null;
_snapshotEndBands = null;
_snapshotPinnedCommands = null;
_snapshotBandViewModels = null;
SettingsModel.SaveSettings(_settingsModel);
Logger.LogDebug("Saved band order to settings");
}
private List<DockBandSettings>? _snapshotStartBands;
private List<DockBandSettings>? _snapshotCenterBands;
private List<DockBandSettings>? _snapshotEndBands;
private List<string>? _snapshotPinnedCommands;
private Dictionary<string, DockBandViewModel>? _snapshotBandViewModels;
/// <summary>
/// Takes a snapshot of the current band order and label settings before editing.
/// Call this when entering edit mode.
/// </summary>
public void SnapshotBandOrder()
{
var dockSettings = _settingsModel.DockSettings;
_snapshotStartBands = dockSettings.StartBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
_snapshotCenterBands = dockSettings.CenterBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
_snapshotEndBands = dockSettings.EndBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
_snapshotPinnedCommands = dockSettings.PinnedCommands.ToList();
// Snapshot band ViewModels so we can restore unpinned bands
// Use a dictionary but handle potential duplicates gracefully
_snapshotBandViewModels = new Dictionary<string, DockBandViewModel>();
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
{
_snapshotBandViewModels.TryAdd(band.Id, band);
}
// Snapshot ShowLabels for all bands
foreach (var band in _snapshotBandViewModels.Values)
{
band.SnapshotShowLabels();
}
Logger.LogDebug($"Snapshot taken: {_snapshotStartBands.Count} start bands, {_snapshotCenterBands.Count} center bands, {_snapshotEndBands.Count} end bands");
}
/// <summary>
/// Restores the band order and label settings from the snapshot taken when entering edit mode.
/// Call this when discarding edit mode changes.
/// </summary>
public void RestoreBandOrder()
{
if (_snapshotStartBands == null || _snapshotCenterBands == null || _snapshotEndBands == null || _snapshotBandViewModels == null || _snapshotPinnedCommands == null)
{
Logger.LogWarning("No snapshot to restore from");
return;
}
// Restore ShowLabels for all snapshotted bands
foreach (var band in _snapshotBandViewModels.Values)
{
band.RestoreShowLabels();
}
var dockSettings = _settingsModel.DockSettings;
// Restore PinnedCommands from snapshot
dockSettings.PinnedCommands.Clear();
dockSettings.PinnedCommands.AddRange(_snapshotPinnedCommands);
// Restore settings from snapshot
dockSettings.StartBands.Clear();
dockSettings.CenterBands.Clear();
dockSettings.EndBands.Clear();
foreach (var bandSnapshot in _snapshotStartBands)
{
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
dockSettings.StartBands.Add(bandSettings);
}
foreach (var bandSnapshot in _snapshotCenterBands)
{
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
dockSettings.CenterBands.Add(bandSettings);
}
foreach (var bandSnapshot in _snapshotEndBands)
{
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
dockSettings.EndBands.Add(bandSettings);
}
// Rebuild UI collections from restored settings using the snapshotted ViewModels
RebuildUICollectionsFromSnapshot();
_snapshotStartBands = null;
_snapshotCenterBands = null;
_snapshotEndBands = null;
_snapshotPinnedCommands = null;
_snapshotBandViewModels = null;
Logger.LogDebug("Restored band order from snapshot");
}
private void RebuildUICollectionsFromSnapshot()
{
if (_snapshotBandViewModels == null)
{
return;
}
var dockSettings = _settingsModel.DockSettings;
StartItems.Clear();
CenterItems.Clear();
EndItems.Clear();
foreach (var bandSettings in dockSettings.StartBands)
{
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
{
StartItems.Add(bandVM);
}
}
foreach (var bandSettings in dockSettings.CenterBands)
{
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
{
CenterItems.Add(bandVM);
}
}
foreach (var bandSettings in dockSettings.EndBands)
{
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
{
EndItems.Add(bandVM);
}
}
}
private void RebuildUICollections()
{
var dockSettings = _settingsModel.DockSettings;
// Create a lookup of all current band ViewModels
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
StartItems.Clear();
CenterItems.Clear();
EndItems.Clear();
foreach (var bandSettings in dockSettings.StartBands)
{
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
{
StartItems.Add(bandVM);
}
}
foreach (var bandSettings in dockSettings.CenterBands)
{
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
{
CenterItems.Add(bandVM);
}
}
foreach (var bandSettings in dockSettings.EndBands)
{
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
{
EndItems.Add(bandVM);
}
}
}
/// <summary>
/// Gets the list of dock bands that are not currently pinned to any section.
/// </summary>
public IEnumerable<TopLevelViewModel> GetAvailableBandsToAdd()
{
// Get IDs of all bands currently in the dock
var pinnedBandIds = new HashSet<string>();
foreach (var band in StartItems)
{
pinnedBandIds.Add(band.Id);
}
foreach (var band in CenterItems)
{
pinnedBandIds.Add(band.Id);
}
foreach (var band in EndItems)
{
pinnedBandIds.Add(band.Id);
}
// Return all dock bands that are not already pinned
return AllItems.Where(tlc => !pinnedBandIds.Contains(tlc.Id));
}
/// <summary>
/// Adds a band to the specified dock section.
/// Does not save to disk - call SaveBandOrder() when done editing.
/// </summary>
public void AddBandToSection(TopLevelViewModel topLevel, DockPinSide targetSide)
{
var bandId = topLevel.Id;
// Check if already in the dock
if (FindBandById(bandId) != null)
{
Logger.LogWarning($"Band {bandId} is already in the dock");
return;
}
// Create settings for the new band
var bandSettings = new DockBandSettings { Id = bandId, ShowLabels = null };
var dockSettings = _settingsModel.DockSettings;
// If this is not an explicit dock band (i.e., it's from TopLevelCommands),
// add it to PinnedCommands so it gets loaded as a dock band on restart
if (!topLevel.IsDockBand && !dockSettings.PinnedCommands.Contains(bandId))
{
dockSettings.PinnedCommands.Add(bandId);
}
// Create the band view model
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
// Add to the appropriate section
switch (targetSide)
{
case DockPinSide.Start:
dockSettings.StartBands.Add(bandSettings);
StartItems.Add(bandVm);
break;
case DockPinSide.Center:
dockSettings.CenterBands.Add(bandSettings);
CenterItems.Add(bandVm);
break;
case DockPinSide.End:
dockSettings.EndBands.Add(bandSettings);
EndItems.Add(bandVm);
break;
}
// Snapshot the new band so it can be removed on discard
bandVm.SnapshotShowLabels();
Logger.LogDebug($"Added band {bandId} to {targetSide} (not saved yet)");
}
/// <summary>
/// Unpins a band from the dock, removing it from whichever section it's in.
/// Does not save to disk - call SaveBandOrder() when done editing.
/// </summary>
public void UnpinBand(DockBandViewModel band)
{
var bandId = band.Id;
var dockSettings = _settingsModel.DockSettings;
// Remove from settings
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
// Also remove from PinnedCommands if it was pinned from TopLevelCommands
dockSettings.PinnedCommands.Remove(bandId);
// Remove from UI collections
StartItems.Remove(band);
CenterItems.Remove(band);
EndItems.Remove(band);
Logger.LogDebug($"Unpinned band {bandId} (not saved yet)");
}
public void ShowException(Exception ex, string? extensionHint = null)
{
var extensionText = extensionHint ?? "<unknown>";
CoreLogger.LogError($"Error in extension {extensionText}", ex);
}
private void DoOnUiThread(Action action)
{
Task.Factory.StartNew(
action,
CancellationToken.None,
TaskCreationOptions.None,
Scheduler);
}
public CommandItemViewModel GetContextMenuForDock()
{
var model = new DockContextMenuItem();
var vm = new CommandItemViewModel(new(model), new(this));
vm.SlowInitializeProperties();
return vm;
}
private sealed partial class DockContextMenuItem : CommandItem
{
public DockContextMenuItem()
{
var editDockCommand = new AnonymousCommand(
action: () =>
{
WeakReferenceMessenger.Default.Send(new EnterDockEditModeMessage());
})
{
Name = "Edit dock", // TODO!Loc
Icon = Icons.EditIcon,
};
var openSettingsCommand = new AnonymousCommand(
action: () =>
{
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Dock"));
})
{
Name = "Dock settings", // TODO!Loc
Icon = Icons.SettingsIcon,
};
MoreCommands = new CommandContextItem[]
{
new CommandContextItem(editDockCommand),
new CommandContextItem(openSettingsCommand),
};
}
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
public partial class DockWindowViewModel : ObservableObject, IDisposable
{
private readonly IThemeService _themeService;
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
[ObservableProperty]
public partial ImageSource? BackgroundImageSource { get; private set; }
[ObservableProperty]
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
[ObservableProperty]
public partial double BackgroundImageOpacity { get; private set; }
[ObservableProperty]
public partial Color BackgroundImageTint { get; private set; }
[ObservableProperty]
public partial double BackgroundImageTintIntensity { get; private set; }
[ObservableProperty]
public partial int BackgroundImageBlurAmount { get; private set; }
[ObservableProperty]
public partial double BackgroundImageBrightness { get; private set; }
[ObservableProperty]
public partial bool ShowBackgroundImage { get; private set; }
[ObservableProperty]
public partial bool ShowColorizationOverlay { get; private set; }
[ObservableProperty]
public partial Color ColorizationColor { get; private set; }
[ObservableProperty]
public partial double ColorizationOpacity { get; private set; }
public DockWindowViewModel(IThemeService themeService)
{
_themeService = themeService;
_themeService.ThemeChanged += ThemeService_ThemeChanged;
UpdateFromThemeSnapshot();
}
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
{
_uiDispatcherQueue.TryEnqueue(UpdateFromThemeSnapshot);
}
private void UpdateFromThemeSnapshot()
{
var snapshot = _themeService.CurrentDockTheme;
BackgroundImageSource = snapshot.BackgroundImageSource;
BackgroundImageStretch = snapshot.BackgroundImageStretch;
BackgroundImageOpacity = snapshot.BackgroundImageOpacity;
BackgroundImageBrightness = snapshot.BackgroundBrightness;
BackgroundImageTint = snapshot.Tint;
BackgroundImageTintIntensity = snapshot.TintIntensity;
BackgroundImageBlurAmount = snapshot.BlurAmount;
ShowBackgroundImage = BackgroundImageSource != null;
// Colorization overlay for transparent backdrop
ShowColorizationOverlay = snapshot.Backdrop == DockBackdrop.Transparent && snapshot.TintIntensity > 0;
ColorizationColor = snapshot.Tint;
ColorizationOpacity = snapshot.TintIntensity;
}
public void Dispose()
{
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,341 @@
// 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 CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
using Windows.UI.ViewManagement;
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// View model for dock appearance settings, controlling theme, backdrop, colorization,
/// and background image settings for the dock.
/// </summary>
public sealed partial class DockAppearanceSettingsViewModel : ObservableObject, IDisposable
{
private readonly SettingsModel _settings;
private readonly DockSettings _dockSettings;
private readonly UISettings _uiSettings;
private readonly IThemeService _themeService;
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread();
private ElementTheme? _elementThemeOverride;
private Color _currentSystemAccentColor;
public ObservableCollection<Color> Swatches => AppearanceSettingsViewModel.WindowsColorSwatches;
public int ThemeIndex
{
get => (int)_dockSettings.Theme;
set => Theme = (UserTheme)value;
}
public UserTheme Theme
{
get => _dockSettings.Theme;
set
{
if (_dockSettings.Theme != value)
{
_dockSettings.Theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ThemeIndex));
Save();
}
}
}
public int BackdropIndex
{
get => (int)_dockSettings.Backdrop;
set => Backdrop = (DockBackdrop)value;
}
public DockBackdrop Backdrop
{
get => _dockSettings.Backdrop;
set
{
if (_dockSettings.Backdrop != value)
{
_dockSettings.Backdrop = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BackdropIndex));
Save();
}
}
}
public ColorizationMode ColorizationMode
{
get => _dockSettings.ColorizationMode;
set
{
if (_dockSettings.ColorizationMode != value)
{
_dockSettings.ColorizationMode = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorizationModeIndex));
OnPropertyChanged(nameof(IsCustomTintVisible));
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
OnPropertyChanged(nameof(IsNoBackgroundVisible));
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
if (value == ColorizationMode.WindowsAccentColor)
{
ThemeColor = _currentSystemAccentColor;
}
IsColorizationDetailsExpanded = value != ColorizationMode.None;
Save();
}
}
}
public int ColorizationModeIndex
{
get => (int)_dockSettings.ColorizationMode;
set => ColorizationMode = (ColorizationMode)value;
}
public Color ThemeColor
{
get => _dockSettings.CustomThemeColor;
set
{
if (_dockSettings.CustomThemeColor != value)
{
_dockSettings.CustomThemeColor = value;
OnPropertyChanged();
if (ColorIntensity == 0)
{
ColorIntensity = 100;
}
Save();
}
}
}
public int ColorIntensity
{
get => _dockSettings.CustomThemeColorIntensity;
set
{
_dockSettings.CustomThemeColorIntensity = value;
OnPropertyChanged();
Save();
}
}
public string BackgroundImagePath
{
get => _dockSettings.BackgroundImagePath ?? string.Empty;
set
{
if (_dockSettings.BackgroundImagePath != value)
{
_dockSettings.BackgroundImagePath = value;
OnPropertyChanged();
if (BackgroundImageOpacity == 0)
{
BackgroundImageOpacity = 100;
}
Save();
}
}
}
public int BackgroundImageOpacity
{
get => _dockSettings.BackgroundImageOpacity;
set
{
if (_dockSettings.BackgroundImageOpacity != value)
{
_dockSettings.BackgroundImageOpacity = value;
OnPropertyChanged();
Save();
}
}
}
public int BackgroundImageBrightness
{
get => _dockSettings.BackgroundImageBrightness;
set
{
if (_dockSettings.BackgroundImageBrightness != value)
{
_dockSettings.BackgroundImageBrightness = value;
OnPropertyChanged();
Save();
}
}
}
public int BackgroundImageBlurAmount
{
get => _dockSettings.BackgroundImageBlurAmount;
set
{
if (_dockSettings.BackgroundImageBlurAmount != value)
{
_dockSettings.BackgroundImageBlurAmount = value;
OnPropertyChanged();
Save();
}
}
}
public BackgroundImageFit BackgroundImageFit
{
get => _dockSettings.BackgroundImageFit;
set
{
if (_dockSettings.BackgroundImageFit != value)
{
_dockSettings.BackgroundImageFit = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BackgroundImageFitIndex));
Save();
}
}
}
public int BackgroundImageFitIndex
{
get => BackgroundImageFit switch
{
BackgroundImageFit.Fill => 1,
_ => 0,
};
set => BackgroundImageFit = value switch
{
1 => BackgroundImageFit.Fill,
_ => BackgroundImageFit.UniformToFill,
};
}
[ObservableProperty]
public partial bool IsColorizationDetailsExpanded { get; set; }
public bool IsCustomTintVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintIntensityVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
public bool IsBackgroundControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.Image;
public bool IsNoBackgroundVisible => _dockSettings.ColorizationMode is ColorizationMode.None;
public bool IsAccentColorControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
public Color EffectiveThemeColor => ColorizationMode switch
{
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
_ => Colors.Transparent,
};
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
public ImageSource? EffectiveBackgroundImageSource =>
ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
public DockAppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
{
_themeService = themeService;
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
_settings = settings;
_dockSettings = settings.DockSettings;
_uiSettings = new UISettings();
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
UpdateAccentColor(_uiSettings);
Reapply();
IsColorizationDetailsExpanded = _dockSettings.ColorizationMode != ColorizationMode.None;
}
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
private void UpdateAccentColor(UISettings sender)
{
_currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent);
if (ColorizationMode == ColorizationMode.WindowsAccentColor)
{
ThemeColor = _currentSystemAccentColor;
}
}
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
{
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}
private void Save()
{
SettingsModel.SaveSettings(_settings);
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}
private void Reapply()
{
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
OnPropertyChanged(nameof(EffectiveThemeColor));
OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount));
// LOAD BEARING:
// We need to cycle through the EffectiveTheme property to force reload of resources.
_elementThemeOverride = ElementTheme.Light;
OnPropertyChanged(nameof(EffectiveTheme));
_elementThemeOverride = ElementTheme.Dark;
OnPropertyChanged(nameof(EffectiveTheme));
_elementThemeOverride = null;
OnPropertyChanged(nameof(EffectiveTheme));
}
[RelayCommand]
private void ResetBackgroundImageProperties()
{
BackgroundImageBrightness = 0;
BackgroundImageBlurAmount = 0;
BackgroundImageFit = BackgroundImageFit.UniformToFill;
BackgroundImageOpacity = 100;
ColorIntensity = 0;
}
public void Dispose()
{
_uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged;
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
}
}

View File

@@ -0,0 +1,18 @@
// 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.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
internal sealed class Icons
{
internal static IconInfo PinIcon => new("\uE718"); // Pin icon
internal static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon
internal static IconInfo SettingsIcon => new("\uE713"); // Settings icon
internal static IconInfo EditIcon => new("\uE70F"); // Edit icon
}

View File

@@ -0,0 +1,7 @@
// 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.UI.ViewModels.Messages;
public record CommandsReloadedMessage();

View File

@@ -0,0 +1,7 @@
// 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.UI.ViewModels.Messages;
public record EnterDockEditModeMessage();

View File

@@ -4,6 +4,6 @@
namespace Microsoft.CmdPal.UI.Messages;
public record OpenSettingsMessage()
public record OpenSettingsMessage(string? Page = null)
{
}

View File

@@ -0,0 +1,7 @@
// 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.UI.ViewModels.Messages;
public record ShowHideDockMessage(bool ShowDock);

View File

@@ -60,6 +60,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Open Command Palette.
/// </summary>
public static string builtin_command_palette_title {
get {
return ResourceManager.GetString("builtin_command_palette_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create another.
/// </summary>
@@ -285,6 +294,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Built-in.
/// </summary>
public static string builtin_extension_name_fallback {
get {
return ResourceManager.GetString("builtin_extension_name_fallback", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Home.
/// </summary>
@@ -429,6 +447,42 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to {0} items.
/// </summary>
public static string dock_item_count_plural {
get {
return ResourceManager.GetString("dock_item_count_plural", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 1 item.
/// </summary>
public static string dock_item_count_singular {
get {
return ResourceManager.GetString("dock_item_count_singular", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pin to dock.
/// </summary>
public static string dock_pin_command_name {
get {
return ResourceManager.GetString("dock_pin_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unpin from dock.
/// </summary>
public static string dock_unpin_command_name {
get {
return ResourceManager.GetString("dock_unpin_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Fallbacks.
/// </summary>

View File

@@ -239,6 +239,30 @@
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
<value>{0} extensions installed</value>
</data>
<data name="builtin_extension_name_fallback" xml:space="preserve">
<value>Built-in</value>
<comment>Fallback name for built-in extensions</comment>
</data>
<data name="dock_pin_command_name" xml:space="preserve">
<value>Pin to dock</value>
<comment>Command name for pinning an item to the dock</comment>
</data>
<data name="dock_unpin_command_name" xml:space="preserve">
<value>Unpin from dock</value>
<comment>Command name for unpinning an item from the dock</comment>
</data>
<data name="dock_item_count_singular" xml:space="preserve">
<value>1 item</value>
<comment>Singular form for item count in dock band</comment>
</data>
<data name="dock_item_count_plural" xml:space="preserve">
<value>{0} items</value>
<comment>Plural form for item count in dock band</comment>
</data>
<data name="builtin_command_palette_title" xml:space="preserve">
<value>Open Command Palette</value>
<comment>Title for the command to open the command palette</comment>
</data>
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
<value>Pick background image</value>
</data>

View File

@@ -0,0 +1,73 @@
// 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.UI.ViewModels.Settings;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Represents a snapshot of dock theme-related visual settings, including accent color, theme preference,
/// backdrop, and background image configuration, for use in rendering the Dock UI.
/// </summary>
public sealed class DockThemeSnapshot
{
/// <summary>
/// Gets the accent tint color used by the Dock visuals.
/// </summary>
public required Color Tint { get; init; }
/// <summary>
/// Gets the intensity of the accent tint color (0-1 range).
/// </summary>
public required float TintIntensity { get; init; }
/// <summary>
/// Gets the configured application theme preference for the Dock.
/// </summary>
public required ElementTheme Theme { get; init; }
/// <summary>
/// Gets the backdrop type for the Dock.
/// </summary>
public required DockBackdrop Backdrop { get; init; }
/// <summary>
/// Gets the image source to render as the background, if any.
/// </summary>
/// <remarks>
/// Returns <see langword="null"/> when no background image is configured.
/// </remarks>
public required ImageSource? BackgroundImageSource { get; init; }
/// <summary>
/// Gets the stretch mode used to lay out the background image.
/// </summary>
public required Stretch BackgroundImageStretch { get; init; }
/// <summary>
/// Gets the opacity applied to the background image.
/// </summary>
/// <value>
/// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
/// </value>
public required double BackgroundImageOpacity { get; init; }
/// <summary>
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
/// </summary>
public required AcrylicBackdropParameters BackdropParameters { get; init; }
/// <summary>
/// Gets the blur amount for the background image.
/// </summary>
public required int BlurAmount { get; init; }
/// <summary>
/// Gets the brightness adjustment for the background (0-1 range).
/// </summary>
public required float BackgroundBrightness { get; init; }
}

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Settings;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
@@ -36,4 +38,9 @@ public interface IThemeService
/// Gets the current theme settings.
/// </summary>
ThemeSnapshot Current { get; }
/// <summary>
/// Gets the current dock theme settings.
/// </summary>
DockThemeSnapshot CurrentDockTheme { get; }
}

View File

@@ -0,0 +1,110 @@
// 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.UI;
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
/// settings are in <see cref="DockBandSettings"/>.
/// </summary>
public class DockSettings
{
public DockSide Side { get; set; } = DockSide.Top;
public DockSize DockSize { get; set; } = DockSize.Small;
public DockSize DockIconsSize { get; set; } = DockSize.Small;
// Theme settings
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
public UserTheme Theme { get; set; } = UserTheme.Default;
public ColorizationMode ColorizationMode { get; set; }
public Color CustomThemeColor { get; set; } = Colors.Transparent;
public int CustomThemeColorIntensity { get; set; } = 100;
public int BackgroundImageOpacity { get; set; } = 20;
public int BackgroundImageBlurAmount { get; set; }
public int BackgroundImageBrightness { get; set; }
public BackgroundImageFit BackgroundImageFit { get; set; }
public string? BackgroundImagePath { get; set; }
// /Theme settings
public List<string> PinnedCommands { get; set; } = [];
public List<DockBandSettings> StartBands { get; set; } = [];
public List<DockBandSettings> CenterBands { get; set; } = [];
public List<DockBandSettings> EndBands { get; set; } = [];
public bool ShowLabels { get; set; } = true;
public DockSettings()
{
// Initialize with default values
PinnedCommands = [
"com.microsoft.cmdpal.winget"
];
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.home" });
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.winget", ShowLabels = false });
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.performanceWidget" });
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.timedate.dockband" });
}
}
/// <summary>
/// Settings for a specific dock band. These are per-band settings stored
/// within the overall <see cref="DockSettings"/>.
/// </summary>
public class DockBandSettings
{
public string Id { get; set; } = string.Empty;
public bool? ShowLabels { get; set; }
/// <summary>
/// Resolves the effective value of <see cref="ShowLabels"/> for this band.
/// If this band doesn't have a specific value set, we'll fall back to the
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
/// </summary>
public bool ResolveShowLabels(bool defaultValue) => ShowLabels ?? defaultValue;
}
public enum DockSide
{
Left = 0,
Top = 1,
Right = 2,
Bottom = 3,
}
public enum DockSize
{
Small,
Medium,
Large,
}
public enum DockBackdrop
{
Transparent,
Acrylic,
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -66,6 +66,11 @@ public partial class SettingsModel : ObservableObject
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
public bool EnableDock { get; set; }
public DockSettings DockSettings { get; set; } = new();
// Theme settings
public UserTheme Theme { get; set; } = UserTheme.Default;
public ColorizationMode ColorizationMode { get; set; }
@@ -84,6 +89,8 @@ public partial class SettingsModel : ObservableObject
public string? BackgroundImagePath { get; set; }
// </Theme settings>
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
@@ -303,6 +310,7 @@ public partial class SettingsModel : ObservableObject
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(Color))]
[JsonSerializable(typeof(HistoryItem))]
[JsonSerializable(typeof(SettingsModel))]
[JsonSerializable(typeof(WindowPosition))]

View File

@@ -4,6 +4,9 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -32,6 +35,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public AppearanceSettingsViewModel Appearance { get; }
public DockAppearanceSettingsViewModel DockAppearance { get; }
public HotkeySettings? Hotkey
{
get => _settings.Hotkey;
@@ -173,6 +178,58 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public DockSide Dock_Side
{
get => _settings.DockSettings.Side;
set
{
_settings.DockSettings.Side = value;
Save();
}
}
public DockSize Dock_DockSize
{
get => _settings.DockSettings.DockSize;
set
{
_settings.DockSettings.DockSize = value;
Save();
}
}
public DockBackdrop Dock_Backdrop
{
get => _settings.DockSettings.Backdrop;
set
{
_settings.DockSettings.Backdrop = value;
Save();
}
}
public bool Dock_ShowLabels
{
get => _settings.DockSettings.ShowLabels;
set
{
_settings.DockSettings.ShowLabels = value;
Save();
}
}
public bool EnableDock
{
get => _settings.EnableDock;
set
{
_settings.EnableDock = value;
Save();
WeakReferenceMessenger.Default.Send(new ShowHideDockMessage(value));
WeakReferenceMessenger.Default.Send(new ReloadCommandsMessage()); // TODO! we need to update the MoreCommands of all top level items, but we don't _really_ want to reload
}
}
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = new();
public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new();
@@ -185,6 +242,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
_topLevelCommandManager = topLevelCommandManager;
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
var activeProviders = GetCommandProviders();
var allProviderSettings = _settings.ProviderSettings;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -44,6 +44,8 @@ public partial class TopLevelCommandManager : ObservableObject,
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
public ObservableCollection<TopLevelViewModel> DockBands { get; set; } = [];
[ObservableProperty]
public partial bool IsLoading { get; private set; } = true;
@@ -79,12 +81,23 @@ public partial class TopLevelCommandManager : ObservableObject,
_builtInCommands.Add(wrapper);
}
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
var objects = await LoadTopLevelCommandsFromProvider(wrapper);
lock (TopLevelCommands)
{
foreach (var c in commands)
if (objects.Commands is IEnumerable<TopLevelViewModel> commands)
{
TopLevelCommands.Add(c);
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
if (objects.DockBands is IEnumerable<TopLevelViewModel> bands)
{
foreach (var c in bands)
{
DockBands.Add(c);
}
}
}
}
@@ -97,7 +110,7 @@ public partial class TopLevelCommandManager : ObservableObject,
}
// May be called from a background thread
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
private async Task<TopLevelObjectSets> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
WeakReference<IPageContext> weakSelf = new(this);
@@ -107,6 +120,7 @@ public partial class TopLevelCommandManager : ObservableObject,
() =>
{
List<TopLevelViewModel> commands = [];
List<TopLevelViewModel> bands = [];
foreach (var item in commandProvider.TopLevelItems)
{
commands.Add(item);
@@ -120,7 +134,15 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
return commands;
foreach (var item in commandProvider.DockBandItems)
{
bands.Add(item);
}
var commandsCount = commands.Count;
var bandsCount = bands.Count;
Logger.LogDebug($"{commandProvider.ProviderId}: Loaded {commandsCount} commands, {bandsCount} bands");
return new TopLevelObjectSets(commands, bands);
},
CancellationToken.None,
TaskCreationOptions.None,
@@ -160,6 +182,8 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
List<TopLevelViewModel> newBands = [.. sender.DockBandItems];
// modify the TopLevelCommands under shared lock; event if we clone it, we don't want
// TopLevelCommands to get modified while we're working on it. Otherwise, we might
// out clone would be stale at the end of this method.
@@ -176,6 +200,13 @@ public partial class TopLevelCommandManager : ObservableObject,
clone.InsertRange(startIndex, newItems);
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
// same idea for DockBands
List<TopLevelViewModel> dockClone = [.. DockBands];
var dockStartIndex = FindIndexForFirstProviderItem(dockClone, sender.ProviderId);
dockClone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
dockClone.InsertRange(dockStartIndex, newBands);
ListHelpers.InPlaceUpdateList(DockBands, dockClone);
}
return;
@@ -222,6 +253,7 @@ public partial class TopLevelCommandManager : ObservableObject,
lock (TopLevelCommands)
{
TopLevelCommands.Clear();
DockBands.Clear();
}
await LoadBuiltinsAsync();
@@ -300,17 +332,34 @@ public partial class TopLevelCommandManager : ObservableObject,
lock (TopLevelCommands)
{
foreach (var commands in commandSets)
foreach (var providerObjects in commandSets)
{
foreach (var c in commands)
var commandsCount = providerObjects.Commands?.Count() ?? 0;
var bandsCount = providerObjects.DockBands?.Count() ?? 0;
Logger.LogDebug($"(some provider) Loaded {commandsCount} commands and {bandsCount} bands");
if (providerObjects.Commands is IEnumerable<TopLevelViewModel> commands)
{
TopLevelCommands.Add(c);
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
if (providerObjects.DockBands is IEnumerable<TopLevelViewModel> bands)
{
foreach (var c in bands)
{
DockBands.Add(c);
}
}
}
}
timer.Stop();
Logger.LogDebug($"Loading extensions took {timer.ElapsedMilliseconds} ms");
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
}
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
@@ -328,7 +377,9 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
private record TopLevelObjectSets(IEnumerable<TopLevelViewModel>? Commands, IEnumerable<TopLevelViewModel>? DockBands);
private async Task<TopLevelObjectSets?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
{
try
{
@@ -408,6 +459,23 @@ public partial class TopLevelCommandManager : ObservableObject,
return null;
}
public TopLevelViewModel? LookupDockBand(string id)
{
// TODO! bad that we're using TopLevelCommands as the object to lock, even for bands
lock (TopLevelCommands)
{
foreach (var command in DockBands)
{
if (command.Id == id)
{
return command;
}
}
}
return null;
}
public void Receive(ReloadCommandsMessage message) =>
ReloadAllCommandsAsync().ConfigureAwait(false);
@@ -426,6 +494,52 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
internal void PinDockBand(TopLevelViewModel bandVm)
{
lock (DockBands)
{
foreach (var existing in DockBands)
{
if (existing.Id == bandVm.Id)
{
// already pinned
Logger.LogDebug($"Dock band '{bandVm.Id}' is already pinned.");
return;
}
}
Logger.LogDebug($"Attempting to pin dock band '{bandVm.Id}' from provider '{bandVm.CommandProviderId}'.");
var providerId = bandVm.CommandProviderId;
var foundProvider = false;
foreach (var provider in CommandProviders)
{
if (provider.Id == providerId)
{
Logger.LogDebug($"Found provider '{providerId}' to pin dock band '{bandVm.Id}'.");
provider.PinDockBand(bandVm);
foundProvider = true;
break;
}
}
if (!foundProvider)
{
Logger.LogWarning($"Could not find provider '{providerId}' to pin dock band '{bandVm.Id}'.");
}
else
{
// Add the band to DockBands if not already present
if (!DockBands.Any(b => b.Id == bandVm.Id))
{
DockBands.Add(bandVm);
}
// Notify DockViewModel to update its collections
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
}
}
}
public void Dispose()
{
_reloadCommandsGate.Dispose();

View File

@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation
// 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 ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -22,6 +24,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
private readonly ProviderSettings _providerSettings;
private readonly IServiceProvider _serviceProvider;
private readonly CommandItemViewModel _commandItemViewModel;
private readonly DockViewModel? _dockViewModel;
private readonly string _commandProviderId;
@@ -45,39 +48,28 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
public CommandPaletteHost ExtensionHost { get; private set; }
public string ExtensionName => ExtensionHost.Extension?.ExtensionDisplayName ?? Properties.Resources.builtin_extension_name_fallback;
public CommandViewModel CommandViewModel => _commandItemViewModel.Command;
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
public string CommandProviderId => _commandProviderId;
public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;
////// ICommandItem
public string Title => _commandItemViewModel.Title;
public string Subtitle => _commandItemViewModel.Subtitle;
public IIconInfo Icon => _commandItemViewModel.Icon;
public IIconInfo Icon => (IIconInfo)IconViewModel;
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
.Select(item =>
{
if (item is ISeparatorContextItem)
{
return item as IContextItem;
}
else if (item is CommandContextItemViewModel commandItem)
{
return commandItem.Model.Unsafe;
}
else
{
return null;
}
}).ToArray();
IContextItem?[] ICommandItem.MoreCommands => BuildContextMenu();
////// IListItem
ITag[] IListItem.Tags => Tags.ToArray();
@@ -176,9 +168,37 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
}
}
// Dock properties
public bool IsDockBand { get; private set; }
public DockBandSettings? DockBandSettings
{
get
{
if (!IsDockBand)
{
return null;
}
var bandSettings = _settings.DockSettings.StartBands
.Concat(_settings.DockSettings.EndBands)
.FirstOrDefault(band => band.Id == this.Id);
if (bandSettings is null)
{
return new DockBandSettings()
{
Id = this.Id,
ShowLabels = true,
};
}
return bandSettings;
}
}
public TopLevelViewModel(
CommandItemViewModel item,
bool isFallback,
TopLevelType topLevelType,
CommandPaletteHost extensionHost,
string commandProviderId,
SettingsModel settings,
@@ -192,23 +212,23 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
_commandProviderId = commandProviderId;
_commandItemViewModel = item;
IsFallback = isFallback;
IsFallback = topLevelType == TopLevelType.Fallback;
IsDockBand = topLevelType == TopLevelType.DockBand;
ExtensionHost = extensionHost;
if (isFallback && commandItem is FallbackCommandItem fallback)
if (IsFallback && commandItem is FallbackCommandItem fallback)
{
_fallbackId = fallback.Id;
}
item.PropertyChanged += Item_PropertyChanged;
// UpdateAlias();
// UpdateHotkey();
// UpdateTags();
_dockViewModel = serviceProvider.GetService<DockViewModel>();
}
internal void InitializeProperties()
{
ItemViewModel.SlowInitializeProperties();
GenerateId();
if (IsFallback)
{
@@ -260,7 +280,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
return;
}
_initialIcon = _commandItemViewModel.Icon;
_initialIcon = (IIconInfo?)_commandItemViewModel.Icon;
if (raiseNotification)
{
@@ -420,4 +440,167 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
};
}
private IContextItem?[] BuildContextMenu()
{
List<IContextItem?> contextItems = new();
foreach (var item in _commandItemViewModel.MoreCommands)
{
if (item is ISeparatorContextItem)
{
contextItems.Add(item as IContextItem);
}
else if (item is CommandContextItemViewModel commandItem)
{
contextItems.Add(commandItem.Model.Unsafe);
}
}
var dockEnabled = _settings.EnableDock;
if (dockEnabled && _dockViewModel is not null)
{
// Add a separator
contextItems.Add(new Separator());
var inStartBands = _settings.DockSettings.StartBands.Any(band => band.Id == this.Id);
var inEndBands = _settings.DockSettings.EndBands.Any(band => band.Id == this.Id);
var alreadyPinned = (inStartBands || inEndBands) &&
_settings.DockSettings.PinnedCommands.Contains(this.Id);
var pinCommand = new PinToDockCommand(
this,
!alreadyPinned,
_dockViewModel,
_settings,
_serviceProvider.GetService<TopLevelCommandManager>()!);
var contextItem = new CommandContextItem(pinCommand);
contextItems.Add(contextItem);
}
return contextItems.ToArray();
}
internal ICommandItem ToPinnedDockBandItem()
{
var item = new PinnedDockItem(item: this, id: Id);
return item;
}
internal TopLevelViewModel CloneAsBand()
{
return new TopLevelViewModel(
_commandItemViewModel,
TopLevelType.DockBand,
ExtensionHost,
_commandProviderId,
_settings,
_providerSettings,
_serviceProvider,
_commandItemViewModel.Model.Unsafe);
}
private sealed partial class PinToDockCommand : InvokableCommand
{
private readonly TopLevelViewModel _topLevelViewModel;
private readonly DockViewModel _dockViewModel;
private readonly SettingsModel _settings;
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly bool _pin;
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
public override string Name => _pin ? Properties.Resources.dock_pin_command_name : Properties.Resources.dock_unpin_command_name;
public PinToDockCommand(
TopLevelViewModel topLevelViewModel,
bool pin,
DockViewModel dockViewModel,
SettingsModel settings,
TopLevelCommandManager topLevelCommandManager)
{
_topLevelViewModel = topLevelViewModel;
_dockViewModel = dockViewModel;
_settings = settings;
_topLevelCommandManager = topLevelCommandManager;
_pin = pin;
}
public override CommandResult Invoke()
{
Logger.LogDebug($"PinToDockCommand.Invoke({_pin}): {_topLevelViewModel.Id}");
if (_pin)
{
PinToDock();
}
else
{
UnpinFromDock();
}
// Notify that the MoreCommands have changed, so the context menu updates
_topLevelViewModel.PropChanged?.Invoke(
_topLevelViewModel,
new PropChangedEventArgs(nameof(ICommandItem.MoreCommands)));
return CommandResult.GoHome();
}
private void PinToDock()
{
// It's possible that the top-level command shares an ID with a
// band. In that case, we don't want to add it to PinnedCommands.
// PinnedCommands is just for top-level commands IDs that aren't
// otherwise bands.
//
// Check the top-level command ID against the bands first.
if (_topLevelCommandManager.DockBands.Any(band => band.Id == _topLevelViewModel.Id))
{
}
else
{
// In this case, the ID isn't another band, so add it to PinnedCommands.
if (!_settings.DockSettings.PinnedCommands.Contains(_topLevelViewModel.Id))
{
_settings.DockSettings.PinnedCommands.Add(_topLevelViewModel.Id);
}
}
// TODO! Deal with "the command ID is already pinned in
// PinnedCommands but not in one of StartBands/EndBands". I think
// we're already avoiding adding it to PinnedCommands above, but I
// think that PinDockBand below will create a duplicate VM for it.
_settings.DockSettings.StartBands.Add(new DockBandSettings()
{
Id = _topLevelViewModel.Id,
ShowLabels = true,
});
// Create a new band VM from our current TLVM. This will allow us to
// update the bands in the CommandProviderWrapper and the TLCM,
// without forcing a whole reload
var bandVm = _topLevelViewModel.CloneAsBand();
_topLevelCommandManager.PinDockBand(bandVm);
_topLevelViewModel.Save();
}
private void UnpinFromDock()
{
_settings.DockSettings.PinnedCommands.Remove(_topLevelViewModel.Id);
_settings.DockSettings.StartBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
_settings.DockSettings.EndBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
_topLevelViewModel.Save();
}
}
}
public enum TopLevelType
{
Normal,
Fallback,
DockBand,
}

View File

@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:services="using:Microsoft.CmdPal.UI.Services">
<Application.Resources>
@@ -11,13 +12,13 @@
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/TextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/Settings.xaml" />
<ResourceDictionary Source="ms-appx:///Controls/Tag.xaml" />
<ResourceDictionary Source="ms-appx:///Controls/KeyVisual/KeyVisual.xaml" />
<ResourceDictionary Source="ms-appx:///Controls/IsEnabledTextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///Dock/DockItemControl.xaml" />
<!-- Default theme dictionary -->
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
<services:MutableOverridesDictionary />
@@ -26,6 +27,14 @@
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="controls:CheckBoxWithDescriptionControl" />
<converters:StringVisibilityConverter
x:Key="StringNotEmptyToVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -12,6 +12,7 @@ using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.ClipboardHistory;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CmdPal.Ext.PerformanceMonitor;
using Microsoft.CmdPal.Ext.Registry;
using Microsoft.CmdPal.Ext.RemoteDesktop;
using Microsoft.CmdPal.Ext.Shell;
@@ -27,6 +28,7 @@ using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
@@ -167,6 +169,7 @@ public partial class App : Application
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
}
private static void AddUIServices(ServiceCollection services)
@@ -201,6 +204,7 @@ public partial class App : Application
// ViewModels
services.AddSingleton<ShellViewModel>();
services.AddSingleton<DockViewModel>();
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
}
}

View File

@@ -98,7 +98,7 @@
<Grid
x:Name="IconRoot"
Margin="3,0,-5,0"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}">
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<Button
x:Name="StatusMessagesButton"
x:Uid="StatusMessagesButton"
@@ -135,7 +135,7 @@
x:Uid="SettingsButton"
Click="SettingsIcon_Clicked"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
VerticalAlignment="Center"
@@ -154,7 +154,7 @@
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
<StackPanel
Grid.Column="2"
Padding="0,0,4,0"

View File

@@ -126,7 +126,7 @@ public sealed partial class CommandBar : UserControl,
private void SettingsIcon_Clicked(object sender, RoutedEventArgs e)
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
}
private void MoreCommandsButton_Clicked(object sender, RoutedEventArgs e)

View File

@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ScrollContainer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<Style x:Key="ScrollButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource FlipViewNextPreviousButtonBackground}" />
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-3" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
AnimatedIcon.State="Normal"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</ContentPresenter.BackgroundTransition>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<Grid
x:Name="RootGrid"
Background="{x:Bind Background, Mode=OneWay}"
BorderBrush="{x:Bind BorderBrush, Mode=OneWay}"
BorderThickness="{x:Bind BorderThickness, Mode=OneWay}"
CornerRadius="{x:Bind CornerRadius, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition x:Name="Row2" Height="Auto" />
</Grid.RowDefinitions>
<!-- Action button - position controlled by visual states -->
<ContentPresenter
x:Name="ActionButtonPresenter"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="2"
Margin="4,0,0,0"
VerticalAlignment="Center"
Content="{x:Bind ActionButton, Mode=OneWay}"
Visibility="{x:Bind ActionButtonVisibility, Mode=OneWay}" />
<Grid
x:Name="ScrollerContainer"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1">
<ScrollViewer
x:Name="scroller"
HorizontalScrollBarVisibility="Hidden"
HorizontalScrollMode="Enabled"
SizeChanged="Scroller_SizeChanged"
VerticalScrollBarVisibility="Hidden"
VerticalScrollMode="Disabled"
ViewChanging="Scroller_ViewChanging">
<Grid x:Name="ContentGrid">
<ContentPresenter Content="{x:Bind Source, Mode=OneWay}" />
</Grid>
</ScrollViewer>
<Button
x:Name="ScrollBackBtn"
Margin="8,0,0,0"
Padding="2,8,2,8"
HorizontalAlignment="Left"
VerticalAlignment="Center"
AutomationProperties.Name="Scroll left"
Click="ScrollBackBtn_Click"
Style="{StaticResource ScrollButtonStyle}"
ToolTipService.ToolTip="Scroll left"
Visibility="Collapsed">
<FontIcon
x:Name="ScrollBackIcon"
FontSize="{ThemeResource FlipViewButtonFontSize}"
Glyph="&#xEDD9;" />
</Button>
<Button
x:Name="ScrollForwardBtn"
Margin="0,0,8,0"
Padding="2,8,2,8"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="Scroll right"
Click="ScrollForwardBtn_Click"
Style="{StaticResource ScrollButtonStyle}"
ToolTipService.ToolTip="Scroll right">
<FontIcon
x:Name="ScrollForwardIcon"
FontSize="{ThemeResource FlipViewButtonFontSize}"
Glyph="&#xEDDA;" />
</Button>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="OrientationStates">
<VisualState x:Name="HorizontalState">
<VisualState.Setters>
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Target="scroller.HorizontalScrollMode" Value="Enabled" />
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
<Setter Target="scroller.VerticalScrollMode" Value="Disabled" />
<Setter Target="ScrollBackBtn.Padding" Value="4,12,4,12" />
<Setter Target="ScrollBackBtn.Margin" Value="8,0,0,0" />
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Left" />
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Center" />
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll left" />
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll left" />
<Setter Target="ScrollBackIcon.Glyph" Value="&#xEDD9;" />
<Setter Target="ScrollForwardBtn.Padding" Value="4,12,4,12" />
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,8,0" />
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Right" />
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Center" />
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll right" />
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll right" />
<Setter Target="ScrollForwardIcon.Glyph" Value="&#xEDDA;" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VerticalState">
<VisualState.Setters>
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Target="scroller.HorizontalScrollMode" Value="Disabled" />
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
<Setter Target="scroller.VerticalScrollMode" Value="Enabled" />
<Setter Target="ScrollBackBtn.Padding" Value="12,4,12,4" />
<Setter Target="ScrollBackBtn.Margin" Value="0,8,0,0" />
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Center" />
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Top" />
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll up" />
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll up" />
<Setter Target="ScrollBackIcon.Glyph" Value="&#xEDDB;" />
<Setter Target="ScrollForwardBtn.Padding" Value="12,4,12,4" />
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,0,8" />
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Center" />
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Bottom" />
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll down" />
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll down" />
<Setter Target="ScrollForwardIcon.Glyph" Value="&#xEDDC;" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="LayoutStates">
<!-- Horizontal + Start: button on right -->
<VisualState x:Name="HorizontalStartState">
<VisualState.Setters>
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="2" />
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
<Setter Target="ActionButtonPresenter.Margin" Value="4,0,0,0" />
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
</VisualState.Setters>
</VisualState>
<!-- Horizontal + End: button on left -->
<VisualState x:Name="HorizontalEndState">
<VisualState.Setters>
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,4,0" />
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
</VisualState.Setters>
</VisualState>
<!-- Vertical + Start: button on bottom -->
<VisualState x:Name="VerticalStartState">
<VisualState.Setters>
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="2" />
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
<Setter Target="ActionButtonPresenter.Margin" Value="0,4,0,0" />
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
</VisualState.Setters>
</VisualState>
<!-- Vertical + End: button on top -->
<VisualState x:Name="VerticalEndState">
<VisualState.Setters>
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,0,4" />
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,222 @@
// 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.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class ScrollContainer : UserControl
{
public enum ScrollContentAlignment
{
Start,
End,
}
public ScrollContainer()
{
InitializeComponent();
Loaded += ScrollContainer_Loaded;
}
private void ScrollContainer_Loaded(object sender, RoutedEventArgs e)
{
UpdateOrientationState();
UpdateLayoutState();
}
public object Source
{
get => (object)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(nameof(Source), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(ScrollContainer), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
public ScrollContentAlignment ContentAlignment
{
get => (ScrollContentAlignment)GetValue(ContentAlignmentProperty);
set => SetValue(ContentAlignmentProperty, value);
}
public static readonly DependencyProperty ContentAlignmentProperty =
DependencyProperty.Register(nameof(ContentAlignment), typeof(ScrollContentAlignment), typeof(ScrollContainer), new PropertyMetadata(ScrollContentAlignment.Start, OnContentAlignmentChanged));
public object ActionButton
{
get => (object)GetValue(ActionButtonProperty);
set => SetValue(ActionButtonProperty, value);
}
public static readonly DependencyProperty ActionButtonProperty =
DependencyProperty.Register(nameof(ActionButton), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
public Visibility ActionButtonVisibility
{
get => (Visibility)GetValue(ActionButtonVisibilityProperty);
set => SetValue(ActionButtonVisibilityProperty, value);
}
public static readonly DependencyProperty ActionButtonVisibilityProperty =
DependencyProperty.Register(nameof(ActionButtonVisibility), typeof(Visibility), typeof(ScrollContainer), new PropertyMetadata(Visibility.Collapsed));
private static void OnContentAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ScrollContainer control)
{
control.UpdateLayoutState();
control.ScrollToAlignment();
}
}
private void ScrollToAlignment()
{
// Reset button visibility
ScrollBackBtn.Visibility = Visibility.Collapsed;
ScrollForwardBtn.Visibility = Visibility.Collapsed;
if (ContentAlignment == ScrollContentAlignment.End)
{
// Scroll to the end
if (Orientation == Orientation.Horizontal)
{
scroller.ChangeView(scroller.ScrollableWidth, null, null, true);
}
else
{
scroller.ChangeView(null, scroller.ScrollableHeight, null, true);
}
}
else
{
// Scroll to the beginning
scroller.ChangeView(0, 0, null, true);
}
// Defer visibility update until after layout
void OnLayoutUpdated(object? sender, object args)
{
scroller.LayoutUpdated -= OnLayoutUpdated;
UpdateScrollButtonsVisibility();
}
scroller.LayoutUpdated += OnLayoutUpdated;
}
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ScrollContainer control)
{
control.UpdateOrientationState();
control.UpdateLayoutState();
control.ScrollToAlignment();
}
}
private void UpdateOrientationState()
{
var stateName = Orientation == Orientation.Horizontal ? "HorizontalState" : "VerticalState";
VisualStateManager.GoToState(this, stateName, true);
}
private void UpdateLayoutState()
{
var isHorizontal = Orientation == Orientation.Horizontal;
var isStart = ContentAlignment == ScrollContentAlignment.Start;
var stateName = (isHorizontal, isStart) switch
{
(true, true) => "HorizontalStartState",
(true, false) => "HorizontalEndState",
(false, true) => "VerticalStartState",
(false, false) => "VerticalEndState",
};
VisualStateManager.GoToState(this, stateName, true);
}
private void Scroller_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
{
UpdateScrollButtonsVisibility(e.FinalView.HorizontalOffset, e.FinalView.VerticalOffset);
}
private void ScrollBackBtn_Click(object sender, RoutedEventArgs e)
{
if (Orientation == Orientation.Horizontal)
{
scroller.ChangeView(scroller.HorizontalOffset - scroller.ViewportWidth, null, null);
}
else
{
scroller.ChangeView(null, scroller.VerticalOffset - scroller.ViewportHeight, null);
}
// Manually focus to ScrollForwardBtn since this button disappears after scrolling to the end.
ScrollForwardBtn.Focus(FocusState.Programmatic);
}
private void ScrollForwardBtn_Click(object sender, RoutedEventArgs e)
{
if (Orientation == Orientation.Horizontal)
{
scroller.ChangeView(scroller.HorizontalOffset + scroller.ViewportWidth, null, null);
}
else
{
scroller.ChangeView(null, scroller.VerticalOffset + scroller.ViewportHeight, null);
}
// Manually focus to ScrollBackBtn since this button disappears after scrolling to the end.
ScrollBackBtn.Focus(FocusState.Programmatic);
}
private void Scroller_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateScrollButtonsVisibility();
}
private void UpdateScrollButtonsVisibility(double? horizontalOffset = null, double? verticalOffset = null)
{
var hOffset = horizontalOffset ?? scroller.HorizontalOffset;
var vOffset = verticalOffset ?? scroller.VerticalOffset;
if (Orientation == Orientation.Horizontal)
{
ScrollBackBtn.Visibility = hOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
ScrollForwardBtn.Visibility = scroller.ScrollableWidth > 0 && hOffset < scroller.ScrollableWidth - 1
? Visibility.Visible
: Visibility.Collapsed;
}
else
{
ScrollBackBtn.Visibility = vOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
ScrollForwardBtn.Visibility = scroller.ScrollableHeight > 0 && vOffset < scroller.ScrollableHeight - 1
? Visibility.Visible
: Visibility.Collapsed;
}
}
}

View File

@@ -0,0 +1,29 @@
// 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.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI.Dock;
internal sealed partial class DockBandTemplateSelector : DataTemplateSelector
{
public DockControl? Control { get; set; }
public DataTemplate? HorizontalTemplate { get; set; }
public DataTemplate? VerticalTemplate { get; set; }
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
{
if (Control is null)
{
return HorizontalTemplate;
}
return Control.ItemsOrientation == Orientation.Horizontal
? HorizontalTemplate
: VerticalTemplate;
}
}

View File

@@ -0,0 +1,463 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Dock.DockControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreVm="using:Microsoft.CmdPal.Core.ViewModels"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<ItemsPanelTemplate x:Key="HorizontalItemsPanel">
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="VerticalItemsPanel">
<StackPanel Orientation="Vertical" Spacing="4" />
</ItemsPanelTemplate>
<DataTemplate x:Key="DeskbandTemplate" x:DataType="dockVm:DockItemViewModel">
<local:DockItemControl
Title="{x:Bind Title, Mode=OneWay}"
RightTapped="BandItem_RightTapped"
Subtitle="{x:Bind Subtitle, Mode=OneWay}"
Tapped="BandItem_Tapped"
ToolTip="{x:Bind Tooltip, Mode=OneWay}">
<local:DockItemControl.Icon>
<cpcontrols:IconBox
x:Name="IconBorder"
Width="16"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
</local:DockItemControl.Icon>
</local:DockItemControl>
</DataTemplate>
<DataTemplate x:Key="HorizontalDockBandTemplate" x:DataType="dockVm:DockBandViewModel">
<ItemsControl
ItemTemplate="{StaticResource DeskbandTemplate}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind Items, Mode=OneWay}" />
</DataTemplate>
<DataTemplate x:Key="VerticalDockBandTemplate" x:DataType="dockVm:DockBandViewModel">
<ItemsControl
ItemTemplate="{StaticResource DeskbandTemplate}"
ItemsPanel="{StaticResource VerticalItemsPanel}"
ItemsSource="{x:Bind Items, Mode=OneWay}" />
</DataTemplate>
<local:DockBandTemplateSelector
x:Key="DockBandTemplateSelector"
Control="{x:Bind}"
HorizontalTemplate="{StaticResource HorizontalDockBandTemplate}"
VerticalTemplate="{StaticResource VerticalDockBandTemplate}" />
<Style x:Key="DockBandListViewStyle" TargetType="ListView">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="0" />
<Setter Property="IsItemClickEnabled" Value="False" />
<Setter Property="SelectionMode" Value="None" />
<!-- Drag properties controlled by code-behind based on IsEditMode -->
<Setter Property="CanDragItems" Value="False" />
<Setter Property="CanReorderItems" Value="False" />
<Setter Property="AllowDrop" Value="False" />
</Style>
<Style x:Key="DockBandListViewItemStyle" TargetType="ListViewItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0,0,4,0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
</Style>
<Style
x:Name="ContextMenuFlyoutStyle"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Style.Setters>
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
</Style.Setters>
</Style>
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
<Flyout
x:Name="ContextMenuFlyout"
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
Opened="ContextMenuFlyout_Opened"
ShouldConstrainToRootBounds="False"
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
<cpcontrols:ContextMenu x:Name="ContextControl" />
</Flyout>
<!-- Edit mode context menu for dock bands -->
<MenuFlyout x:Name="EditModeContextMenu" ShouldConstrainToRootBounds="False">
<MenuFlyoutItem
x:Name="ShowLabelsMenuItem"
Click="ShowLabelsMenuItem_Click"
Text="Show labels">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE8EC;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
x:Name="HideLabelsMenuItem"
Click="HideLabelsMenuItem_Click"
Text="Hide labels">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE8EC;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Name="UnpinBandMenuItem"
Click="UnpinBandMenuItem_Click"
Text="Unpin">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE77A;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
<!-- Add band flyout - used in edit mode to add bands to dock sections -->
<Flyout
x:Name="AddBandFlyout"
Placement="Bottom"
ShouldConstrainToRootBounds="False">
<StackPanel Width="320">
<TextBlock
x:Name="NoAvailableBandsText"
Padding="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="No commands available to pin"
TextAlignment="Center"
Visibility="Collapsed" />
<ListView
x:Name="AddBandListView"
MaxHeight="300"
HorizontalAlignment="Stretch"
IsItemClickEnabled="True"
ItemClick="AddBandListView_ItemClick"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:TopLevelViewModel">
<Grid Padding="4" ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<cpcontrols:IconBox
Width="20"
Height="20"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Flyout>
</ResourceDictionary>
</UserControl.Resources>
<Grid
x:Name="RootGrid"
BorderThickness="0,0,0,1"
RightTapped="RootGrid_RightTapped">
<!-- Edit Mode Overlay - shown when in edit mode -->
<Grid
x:Name="ContentGrid"
Margin="4"
Padding="4,0,4,0"
Background="Transparent"
RightTapped="RootGrid_RightTapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<cpcontrols:ScrollContainer
x:Name="StartScroller"
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Left"
VerticalAlignment="Stretch">
<cpcontrols:ScrollContainer.ActionButton>
<Button
x:Name="StartAddButton"
Click="AddBandButton_Click"
Style="{StaticResource SubtleButtonStyle}"
Tag="Start"
ToolTipService.ToolTip="Add band to Start">
<FontIcon FontSize="12" Glyph="&#xE710;" />
</Button>
</cpcontrols:ScrollContainer.ActionButton>
<cpcontrols:ScrollContainer.Source>
<ListView
x:Name="StartListView"
HorizontalAlignment="Stretch"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
DragOver="BandListView_DragOver"
Drop="StartListView_Drop"
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
ItemTemplateSelector="{StaticResource DockBandTemplateSelector}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind ViewModel.StartItems, Mode=OneWay}"
SelectionMode="None"
Style="{StaticResource DockBandListViewStyle}" />
</cpcontrols:ScrollContainer.Source>
</cpcontrols:ScrollContainer>
<cpcontrols:ScrollContainer
x:Name="CenterScroller"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
MinWidth="48"
HorizontalAlignment="Center"
VerticalAlignment="Stretch">
<cpcontrols:ScrollContainer.ActionButton>
<Button
x:Name="CenterAddButton"
Click="AddBandButton_Click"
Style="{StaticResource SubtleButtonStyle}"
Tag="Center"
ToolTipService.ToolTip="Add band to Center">
<FontIcon FontSize="12" Glyph="&#xE710;" />
</Button>
</cpcontrols:ScrollContainer.ActionButton>
<cpcontrols:ScrollContainer.Source>
<ListView
x:Name="CenterListView"
HorizontalAlignment="Stretch"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
DragOver="BandListView_DragOver"
Drop="CenterListView_Drop"
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
ItemTemplateSelector="{StaticResource DockBandTemplateSelector}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind ViewModel.CenterItems, Mode=OneWay}"
SelectionMode="None"
Style="{StaticResource DockBandListViewStyle}" />
</cpcontrols:ScrollContainer.Source>
</cpcontrols:ScrollContainer>
<cpcontrols:ScrollContainer
x:Name="EndScroller"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
ContentAlignment="End">
<cpcontrols:ScrollContainer.ActionButton>
<Button
x:Name="EndAddButton"
Click="AddBandButton_Click"
Style="{StaticResource SubtleButtonStyle}"
Tag="End"
ToolTipService.ToolTip="Add band to End">
<FontIcon FontSize="12" Glyph="&#xE710;" />
</Button>
</cpcontrols:ScrollContainer.ActionButton>
<cpcontrols:ScrollContainer.Source>
<ListView
x:Name="EndListView"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
DragOver="BandListView_DragOver"
Drop="EndListView_Drop"
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
ItemTemplateSelector="{StaticResource DockBandTemplateSelector}"
ItemsPanel="{StaticResource HorizontalItemsPanel}"
ItemsSource="{x:Bind ViewModel.EndItems, Mode=OneWay}"
SelectionMode="None"
Style="{StaticResource DockBandListViewStyle}">
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
</cpcontrols:ScrollContainer.Source>
</cpcontrols:ScrollContainer>
<!-- To do: remove the X button -->
<TeachingTip
x:Name="EditButtonsTeachingTip"
MinWidth="0"
PreferredPlacement="Bottom"
ShouldConstrainToRootBounds="False"
Target="{x:Bind ContentGrid}">
<!--<TeachingTip.CloseButtonStyle>
-->
<!-- Workaround to hide the close button -->
<!--
<Style BasedOn="{StaticResource DefaultButtonStyle}" TargetType="Button">
<Setter Property="Visibility" Value="Collapsed" />
<Setter Property="Width" Value="0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Foreground" Value="Red" />
<Setter Property="Height" Value="0" />
<Setter Property="MinHeight" Value="0" />
</Style>
</TeachingTip.CloseButtonStyle>-->
<TeachingTip.Content>
<StackPanel
x:Name="EditButtonsPanel"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="4">
<Button
HorizontalAlignment="Stretch"
Click="DoneEditingButton_Click"
Content="Save"
Style="{StaticResource AccentButtonStyle}" />
<Button
HorizontalAlignment="Stretch"
Click="DiscardEditingButton_Click"
Content="Discard" />
</StackPanel>
</TeachingTip.Content>
</TeachingTip>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DockOrientation">
<VisualState x:Name="DockOnTop">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Top" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="DockOnBottom">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Bottom" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="RootGrid.BorderThickness" Value="0,1,0,0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DockOnLeft">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Left" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="StartScroller.Orientation" Value="Vertical" />
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
<Setter Target="StartScroller.Orientation" Value="Vertical" />
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
<Setter Target="EndScroller.Orientation" Value="Vertical" />
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
<Setter Target="RootGrid.BorderThickness" Value="0,0,1,0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DockOnRight">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Right" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="StartScroller.Orientation" Value="Vertical" />
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
<Setter Target="StartScroller.Orientation" Value="Vertical" />
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
<Setter Target="EndScroller.Orientation" Value="Vertical" />
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
<Setter Target="RootGrid.BorderThickness" Value="1,0,0,0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<!-- Edit Mode Visual States -->
<VisualStateGroup x:Name="EditModeStates">
<VisualState x:Name="EditModeOff" />
<VisualState x:Name="EditModeOn">
<VisualState.Setters>
<Setter Target="StartScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Target="StartScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Target="StartScroller.BorderThickness" Value="1" />
<Setter Target="StartScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
<Setter Target="CenterScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Target="CenterScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Target="CenterScroller.BorderThickness" Value="1" />
<Setter Target="CenterScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
<Setter Target="EndScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Target="EndScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Target="EndScroller.BorderThickness" Value="1" />
<Setter Target="EndScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
<Setter Target="StartScroller.ActionButtonVisibility" Value="Visible" />
<Setter Target="CenterScroller.ActionButtonVisibility" Value="Visible" />
<Setter Target="EndScroller.ActionButtonVisibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,563 @@
// 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 System.Runtime.InteropServices;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.Dock;
public sealed partial class DockControl : UserControl, IRecipient<CloseContextMenuMessage>, IRecipient<EnterDockEditModeMessage>
{
private DockViewModel _viewModel;
internal DockViewModel ViewModel => _viewModel;
public static readonly DependencyProperty ItemsOrientationProperty =
DependencyProperty.Register(nameof(ItemsOrientation), typeof(Orientation), typeof(DockControl), new PropertyMetadata(Orientation.Horizontal, OnItemsOrientationChanged));
public Orientation ItemsOrientation
{
get => (Orientation)GetValue(ItemsOrientationProperty);
set => SetValue(ItemsOrientationProperty, value);
}
private static void OnItemsOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockControl control)
{
control.UpdateBandTemplates();
}
}
public static readonly DependencyProperty DockSideProperty =
DependencyProperty.Register(nameof(DockSide), typeof(DockSide), typeof(DockControl), new PropertyMetadata(DockSide.Top));
public DockSide DockSide
{
get => (DockSide)GetValue(DockSideProperty);
set => SetValue(DockSideProperty, value);
}
public static readonly DependencyProperty IsEditModeProperty =
DependencyProperty.Register(nameof(IsEditMode), typeof(bool), typeof(DockControl), new PropertyMetadata(false, OnIsEditModeChanged));
public bool IsEditMode
{
get => (bool)GetValue(IsEditModeProperty);
set => SetValue(IsEditModeProperty, value);
}
private static void OnIsEditModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockControl control && e.NewValue is bool isEditMode)
{
control.UpdateEditMode(isEditMode);
}
}
private void UpdateBandTemplates()
{
var panelKey = ItemsOrientation == Orientation.Horizontal
? "HorizontalItemsPanel"
: "VerticalItemsPanel";
if ((ItemsPanelTemplate)App.Current.Resources[panelKey] is ItemsPanelTemplate panelTemplate)
{
StartListView.ItemsPanel = panelTemplate;
CenterListView.ItemsPanel = panelTemplate;
EndListView.ItemsPanel = panelTemplate;
}
}
internal DockControl(DockViewModel viewModel)
{
_viewModel = viewModel;
InitializeComponent();
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
// Start with edit mode disabled - normal click behavior
UpdateEditMode(false);
}
public void Receive(EnterDockEditModeMessage message)
{
// Message may arrive from a background thread, dispatch to UI thread
DispatcherQueue.TryEnqueue(() =>
{
EnterEditMode();
});
}
private void UpdateEditMode(bool isEditMode)
{
// Enable/disable drag-and-drop based on edit mode
StartListView.CanDragItems = isEditMode;
StartListView.CanReorderItems = isEditMode;
StartListView.AllowDrop = isEditMode;
CenterListView.CanDragItems = isEditMode;
CenterListView.CanReorderItems = isEditMode;
CenterListView.AllowDrop = isEditMode;
EndListView.CanDragItems = isEditMode;
EndListView.CanReorderItems = isEditMode;
EndListView.AllowDrop = isEditMode;
if (isEditMode)
{
EditButtonsTeachingTip.PreferredPlacement = DockSide switch
{
DockSide.Left => TeachingTipPlacementMode.Right,
DockSide.Right => TeachingTipPlacementMode.Left,
DockSide.Top => TeachingTipPlacementMode.Bottom,
DockSide.Bottom => TeachingTipPlacementMode.Top,
_ => TeachingTipPlacementMode.Auto,
};
}
EditButtonsTeachingTip.IsOpen = isEditMode;
// Update visual state
VisualStateManager.GoToState(this, isEditMode ? "EditModeOn" : "EditModeOff", true);
}
internal void EnterEditMode()
{
// Snapshot current state so we can restore on discard
ViewModel.SnapshotBandOrder();
IsEditMode = true;
}
internal void ExitEditMode()
{
IsEditMode = false;
// Save all changes when exiting edit mode
ViewModel.SaveBandOrder();
}
internal void DiscardEditMode()
{
IsEditMode = false;
// Restore the original band order from snapshot
ViewModel.RestoreBandOrder();
}
private void DoneEditingButton_Click(object sender, RoutedEventArgs e)
{
ExitEditMode();
}
private void DiscardEditingButton_Click(object sender, RoutedEventArgs e)
{
DiscardEditMode();
}
internal void UpdateSettings(DockSettings settings)
{
DockSide = settings.Side;
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
ItemsOrientation = isHorizontal ? Orientation.Horizontal : Orientation.Vertical;
if (settings.Backdrop == DockBackdrop.Transparent)
{
RootGrid.BorderBrush = new SolidColorBrush(Colors.Transparent);
}
}
private void BandItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
// Ignore clicks when in edit mode - allow drag behavior instead
if (IsEditMode)
{
return;
}
if (sender is DockItemControl dockItem && dockItem.DataContext is DockItemViewModel item)
{
// Use the center of the border as the point to open at
var borderPos = dockItem.TransformToVisual(null).TransformPoint(new Point(0, 0));
var borderCenter = new Point(
borderPos.X + (dockItem.ActualWidth / 2),
borderPos.Y + (dockItem.ActualHeight / 2));
InvokeItem(item, borderCenter);
e.Handled = true;
}
}
// Stores the band that was right-clicked for edit mode context menu
private DockBandViewModel? _editModeContextBand;
private void BandItem_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
{
if (sender is DockItemControl dockItem && dockItem.DataContext is DockItemViewModel item)
{
// In edit mode, show the edit mode context menu (show/hide labels)
if (IsEditMode)
{
// Find the parent DockBandViewModel for this item
_editModeContextBand = FindParentBand(item);
if (_editModeContextBand != null)
{
// Update menu item visibility based on current state
ShowLabelsMenuItem.Visibility = _editModeContextBand.ShowLabels ? Visibility.Collapsed : Visibility.Visible;
HideLabelsMenuItem.Visibility = _editModeContextBand.ShowLabels ? Visibility.Visible : Visibility.Collapsed;
EditModeContextMenu.ShowAt(
dockItem,
new FlyoutShowOptions()
{
ShowMode = FlyoutShowMode.Standard,
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
});
e.Handled = true;
}
return;
}
// Normal mode - show the command context menu
if (item.HasMoreCommands)
{
ContextControl.ViewModel.SelectedItem = item;
ContextMenuFlyout.ShowAt(
dockItem,
new FlyoutShowOptions()
{
ShowMode = FlyoutShowMode.Standard,
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
});
e.Handled = true;
}
}
}
private DockBandViewModel? FindParentBand(DockItemViewModel item)
{
// Search all bands to find which one contains this item
foreach (var band in ViewModel.StartItems)
{
if (band.Items.Contains(item))
{
return band;
}
}
foreach (var band in ViewModel.CenterItems)
{
if (band.Items.Contains(item))
{
return band;
}
}
foreach (var band in ViewModel.EndItems)
{
if (band.Items.Contains(item))
{
return band;
}
}
return null;
}
private void ShowLabelsMenuItem_Click(object sender, RoutedEventArgs e)
{
if (_editModeContextBand != null)
{
_editModeContextBand.ShowLabels = true;
}
}
private void HideLabelsMenuItem_Click(object sender, RoutedEventArgs e)
{
if (_editModeContextBand != null)
{
_editModeContextBand.ShowLabels = false;
}
}
private void UnpinBandMenuItem_Click(object sender, RoutedEventArgs e)
{
if (_editModeContextBand != null)
{
ViewModel.UnpinBand(_editModeContextBand);
_editModeContextBand = null;
}
}
private void InvokeItem(DockItemViewModel item, Point pos)
{
var command = item.Command;
try
{
var isPage = command.Model.Unsafe is not IInvokableCommand invokable;
if (isPage)
{
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos));
}
PerformCommandMessage m = new(command.Model);
m.WithAnimation = false;
m.TransientPage = true;
WeakReferenceMessenger.Default.Send(m);
}
catch (COMException e)
{
Logger.LogError("Error invoking dock command", e);
}
}
private void ContextMenuFlyout_Opened(object sender, object e)
{
// We need to wait until our flyout is opened to try and toss focus
// at its search box. The control isn't in the UI tree before that
ContextControl.FocusSearchBox();
}
public void Receive(CloseContextMenuMessage message)
{
if (ContextMenuFlyout.IsOpen)
{
ContextMenuFlyout.Hide();
}
}
private void RootGrid_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
{
var pos = e.GetPosition(null);
var item = this.ViewModel.GetContextMenuForDock();
if (item.HasMoreCommands)
{
ContextControl.ViewModel.SelectedItem = item;
ContextMenuFlyout.ShowAt(
this.RootGrid,
new FlyoutShowOptions()
{
ShowMode = FlyoutShowMode.Standard,
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
Position = pos,
});
e.Handled = true;
}
}
private DockBandViewModel? _draggedBand;
private void BandListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
if (e.Items.Count > 0 && e.Items[0] is DockBandViewModel band)
{
_draggedBand = band;
e.Data.RequestedOperation = DataPackageOperation.Move;
}
}
private void BandListView_DragOver(object sender, DragEventArgs e)
{
if (_draggedBand != null)
{
e.AcceptedOperation = DataPackageOperation.Move;
}
}
private void BandListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
{
// Reordering within the same list is handled automatically by ListView
// We just need to sync the ViewModel order without saving
if (args.DropResult == DataPackageOperation.Move && _draggedBand != null)
{
DockPinSide targetSide;
ObservableCollection<DockBandViewModel> targetCollection;
if (sender == StartListView)
{
targetSide = DockPinSide.Start;
targetCollection = ViewModel.StartItems;
}
else if (sender == CenterListView)
{
targetSide = DockPinSide.Center;
targetCollection = ViewModel.CenterItems;
}
else
{
targetSide = DockPinSide.End;
targetCollection = ViewModel.EndItems;
}
// Find the new index and sync ViewModel (without saving)
var newIndex = targetCollection.IndexOf(_draggedBand);
if (newIndex >= 0)
{
ViewModel.SyncBandPosition(_draggedBand, targetSide, newIndex);
}
}
_draggedBand = null;
}
private void StartListView_Drop(object sender, DragEventArgs e)
{
HandleCrossListDrop(DockPinSide.Start, e);
}
private void CenterListView_Drop(object sender, DragEventArgs e)
{
HandleCrossListDrop(DockPinSide.Center, e);
}
private void EndListView_Drop(object sender, DragEventArgs e)
{
HandleCrossListDrop(DockPinSide.End, e);
}
private void HandleCrossListDrop(DockPinSide targetSide, DragEventArgs e)
{
if (_draggedBand == null)
{
return;
}
// Check which list the band is currently in
var isInStart = ViewModel.StartItems.Contains(_draggedBand);
var isInCenter = ViewModel.CenterItems.Contains(_draggedBand);
var isInEnd = ViewModel.EndItems.Contains(_draggedBand);
DockPinSide sourceSide;
if (isInStart)
{
sourceSide = DockPinSide.Start;
}
else if (isInCenter)
{
sourceSide = DockPinSide.Center;
}
else
{
sourceSide = DockPinSide.End;
}
// Only handle cross-list drops here; same-list reorders are handled in DragItemsCompleted
if (sourceSide != targetSide)
{
// Calculate drop index based on drop position
var targetListView = targetSide switch
{
DockPinSide.Start => StartListView,
DockPinSide.Center => CenterListView,
_ => EndListView,
};
var targetCollection = targetSide switch
{
DockPinSide.Start => ViewModel.StartItems,
DockPinSide.Center => ViewModel.CenterItems,
_ => ViewModel.EndItems,
};
var dropIndex = GetDropIndex(targetListView, e, targetCollection.Count);
// Move the band to the new side (without saving - save happens on Done)
ViewModel.MoveBandWithoutSaving(_draggedBand, targetSide, dropIndex);
e.Handled = true;
}
}
private int GetDropIndex(ListView listView, DragEventArgs e, int itemCount)
{
var position = e.GetPosition(listView);
// Find the item at the drop position
for (var i = 0; i < itemCount; i++)
{
if (listView.ContainerFromIndex(i) is ListViewItem container)
{
var itemBounds = container.TransformToVisual(listView).TransformBounds(
new Rect(0, 0, container.ActualWidth, container.ActualHeight));
if (ItemsOrientation == Orientation.Horizontal)
{
// For horizontal layout, check X position
if (position.X < itemBounds.X + (itemBounds.Width / 2))
{
return i;
}
}
else
{
// For vertical layout, check Y position
if (position.Y < itemBounds.Y + (itemBounds.Height / 2))
{
return i;
}
}
}
}
// If we're past all items, insert at the end
return itemCount;
}
// Tracks which section (Start/Center/End) the add button was clicked for
private DockPinSide _addBandTargetSide;
private void AddBandButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is string sideTag)
{
_addBandTargetSide = sideTag switch
{
"Start" => DockPinSide.Start,
"Center" => DockPinSide.Center,
"End" => DockPinSide.End,
_ => DockPinSide.Center,
};
// Populate the list with available bands (not already in the dock)
var availableBands = ViewModel.GetAvailableBandsToAdd().ToList();
AddBandListView.ItemsSource = availableBands;
// Show/hide empty state text based on whether there are bands to add
var hasAvailableBands = availableBands.Count > 0;
NoAvailableBandsText.Visibility = hasAvailableBands ? Visibility.Collapsed : Visibility.Visible;
AddBandListView.Visibility = hasAvailableBands ? Visibility.Visible : Visibility.Collapsed;
// Show the flyout
AddBandFlyout.ShowAt(button);
}
}
private void AddBandListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is TopLevelViewModel topLevel)
{
// Add the band to the target section
ViewModel.AddBandToSection(topLevel, _addBandTargetSide);
// Close the flyout
AddBandFlyout.Hide();
}
}
}

View File

@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.CmdPal.UI.Dock">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#0FFFFFFF" />
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#0BFFFFFF" />
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.33" Color="#0FFFFFFF" />
<GradientStop Offset="1.0" Color="#19FFFFFF" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#0BFFFFFF" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#80FFFFFF" />
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#4DFFFFFF" />
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.33" Color="#08000000" />
<GradientStop Offset="1.0" Color="#17000000" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#05000000" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="{StaticResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="DockItemBorderBrushPointerOver" Color="{StaticResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style BasedOn="{StaticResource DefaultDockItemControlStyle}" TargetType="local:DockItemControl" />
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
<Style.Setters>
<Setter Property="Background" Value="{ThemeResource DockItemBackground}" />
<Setter Property="BorderBrush" Value="{ThemeResource DockItemBorderBrush}" />
<Setter Property="Padding" Value="4,0,4,0" />
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="TextVisibility" Value="Visible" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockItemControl">
<Grid
x:Name="PART_RootGrid"
MaxWidth="120"
Padding="{TemplateBinding Padding}"
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
ToolTipService.ToolTip="{TemplateBinding ToolTip}">
<Grid
x:Name="ContentGrid"
AutomationProperties.Name="{TemplateBinding Title}"
Background="Transparent"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Icon -->
<ContentPresenter
x:Name="IconPresenter"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}" />
<!-- Text (Title + Subtitle) -->
<StackPanel
x:Name="TextPanel"
Grid.Column="1"
VerticalAlignment="Center"
Visibility="{TemplateBinding TextVisibility}">
<TextBlock
x:Name="TitleText"
MaxWidth="100"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Text="{TemplateBinding Title}"
TextAlignment="Left"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Name="SubtitleText"
MaxWidth="100"
Margin="0,-4,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="10"
Foreground="{ThemeResource TextFillColorTertiary}"
Text="{TemplateBinding Subtitle}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackground}" />
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBackground}" />
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
<Setter Target="TitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
<Setter Target="SubtitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="TextVisibilityStates">
<VisualState x:Name="TextVisible" />
<VisualState x:Name="TextHidden">
<VisualState.Setters>
<Setter Target="ContentGrid.ColumnSpacing" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="IconVisibilityStates">
<VisualState x:Name="IconVisible" />
<VisualState x:Name="IconHidden">
<VisualState.Setters>
<Setter Target="ContentGrid.ColumnSpacing" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,212 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Markup;
namespace Microsoft.CmdPal.UI.Dock;
[ContentProperty(Name = nameof(Icon))]
public sealed partial class DockItemControl : Control
{
public DockItemControl()
{
DefaultStyleKey = typeof(DockItemControl);
}
public static readonly DependencyProperty ToolTipProperty =
DependencyProperty.Register(nameof(ToolTip), typeof(string), typeof(DockItemControl), new PropertyMetadata(null));
public string ToolTip
{
get => (string)GetValue(ToolTipProperty);
set => SetValue(ToolTipProperty, value);
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly DependencyProperty SubtitleProperty =
DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
public string Subtitle
{
get => (string)GetValue(SubtitleProperty);
set => SetValue(SubtitleProperty, value);
}
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(nameof(Icon), typeof(object), typeof(DockItemControl), new PropertyMetadata(null, OnIconPropertyChanged));
public object Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly DependencyProperty TextVisibilityProperty =
DependencyProperty.Register(nameof(TextVisibility), typeof(Visibility), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
public Visibility TextVisibility
{
get => (Visibility)GetValue(TextVisibilityProperty);
set => SetValue(TextVisibilityProperty, value);
}
private const string IconPresenterName = "IconPresenter";
private const string TitleTextName = "TitleText";
private const string SubtitleTextName = "SubtitleText";
private FrameworkElement? _iconPresenter;
private FrameworkElement? _titleText;
private FrameworkElement? _subtitleText;
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockItemControl control)
{
control.UpdateTextVisibility();
control.UpdateAlignment();
}
}
private static void OnIconPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockItemControl control)
{
control.UpdateIconVisibility();
control.UpdateAlignment();
}
}
internal bool HasTitle => !string.IsNullOrEmpty(Title);
internal bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
internal bool HasText => HasTitle || HasSubtitle;
private void UpdateTextVisibility()
{
if (_titleText is not null)
{
_titleText.Visibility = HasTitle ? Visibility.Visible : Visibility.Collapsed;
}
if (_subtitleText is not null)
{
_subtitleText.Visibility = HasSubtitle ? Visibility.Visible : Visibility.Collapsed;
}
UpdateTextVisibilityState();
}
private void UpdateTextVisibilityState()
{
var hasText = !string.IsNullOrEmpty(Title) || !string.IsNullOrEmpty(Subtitle);
VisualStateManager.GoToState(this, hasText ? "TextVisible" : "TextHidden", true);
}
private void UpdateIconVisibility()
{
if (_iconPresenter is not null)
{
// n.b. this might be wrong - I think we always have an Icon (an IconBox),
// we need to check if the box has an icon
_iconPresenter.Visibility = Icon is null ? Visibility.Collapsed : Visibility.Visible;
}
UpdateIconVisibilityState();
}
private void UpdateIconVisibilityState()
{
var hasIcon = Icon is not null;
VisualStateManager.GoToState(this, hasIcon ? "IconVisible" : "IconHidden", true);
}
private void UpdateAlignment()
{
// If this item has both an icon and a label, left align so that the
// icons don't wobble if the text changes.
//
// Otherwise, center align.
var requestedTheme = ActualTheme;
var isLight = requestedTheme == ElementTheme.Light;
var showText = HasText;
if (Icon is IconBox icoBox &&
icoBox.DataContext is DockItemViewModel item &&
item.Icon is IconInfoViewModel icon)
{
var showIcon = icon is not null && icon.HasIcon(isLight);
if (showText && showIcon)
{
HorizontalAlignment = HorizontalAlignment.Left;
return;
}
}
HorizontalAlignment = HorizontalAlignment.Center;
}
private void UpdateAllVisibility()
{
UpdateTextVisibility();
UpdateIconVisibility();
UpdateAlignment();
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
IsEnabledChanged -= OnIsEnabledChanged;
PointerEntered += Control_PointerEntered;
PointerExited += Control_PointerExited;
IsEnabledChanged += OnIsEnabledChanged;
// Get template children for visibility updates
_iconPresenter = GetTemplateChild(IconPresenterName) as FrameworkElement;
_titleText = GetTemplateChild(TitleTextName) as FrameworkElement;
_subtitleText = GetTemplateChild(SubtitleTextName) as FrameworkElement;
// Set initial visibility
UpdateAllVisibility();
}
private void Control_PointerEntered(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "PointerOver", true);
}
private void Control_PointerExited(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "Normal", true);
}
protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
if (IsEnabled)
{
base.OnPointerPressed(e);
VisualStateManager.GoToState(this, "Pressed", true);
}
}
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
}
}

View File

@@ -0,0 +1,68 @@
// 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.UI.ViewModels.Settings;
using Windows.Win32;
using WinUIEx;
namespace Microsoft.CmdPal.UI.Dock;
internal static class DockSettingsToViews
{
public static double WidthForSize(DockSize size)
{
return size switch
{
DockSize.Small => 128,
DockSize.Medium => 192,
DockSize.Large => 256,
_ => throw new NotImplementedException(),
};
}
public static double HeightForSize(DockSize size)
{
return size switch
{
DockSize.Small => 32,
DockSize.Medium => 54,
DockSize.Large => 76,
_ => throw new NotImplementedException(),
};
}
public static double IconSizeForSize(DockSize size)
{
return size switch
{
DockSize.Small => 32 / 2,
DockSize.Medium => 54 / 2,
DockSize.Large => 76 / 2,
_ => throw new NotImplementedException(),
};
}
public static Microsoft.UI.Xaml.Media.SystemBackdrop? GetSystemBackdrop(DockBackdrop backdrop)
{
return backdrop switch
{
DockBackdrop.Transparent => new TransparentTintBackdrop(),
DockBackdrop.Acrylic => null, // new DesktopAcrylicBackdrop(),
_ => throw new NotImplementedException(),
};
}
public static uint GetAppBarEdge(DockSide side)
{
return side switch
{
DockSide.Left => PInvoke.ABE_LEFT,
DockSide.Top => PInvoke.ABE_TOP,
DockSide.Right => PInvoke.ABE_RIGHT,
DockSide.Bottom => PInvoke.ABE_BOTTOM,
_ => throw new NotImplementedException(),
};
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
x:Class="Microsoft.CmdPal.UI.Dock.DockWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
xmlns:winuiex="using:WinUIEx"
Title="PowerDock"
Closed="DockWindow_Closed"
mc:Ignorable="d">
<Grid
x:Name="Root"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<!-- Colorization overlay for transparent backdrop -->
<Border
x:Name="ColorizationOverlay"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsHitTestVisible="False"
Opacity="{x:Bind WindowViewModel.ColorizationOpacity, Mode=OneWay}"
Visibility="{x:Bind WindowViewModel.ShowColorizationOverlay, Mode=OneWay}">
<Border.Background>
<SolidColorBrush Color="{x:Bind WindowViewModel.ColorizationColor, Mode=OneWay}" />
</Border.Background>
</Border>
<cpcontrols:BlurImageControl
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BlurAmount="{x:Bind WindowViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
ImageBrightness="{x:Bind WindowViewModel.BackgroundImageBrightness, Mode=OneWay}"
ImageOpacity="{x:Bind WindowViewModel.BackgroundImageOpacity, Mode=OneWay}"
ImageSource="{x:Bind WindowViewModel.BackgroundImageSource, Mode=OneWay}"
ImageStretch="{x:Bind WindowViewModel.BackgroundImageStretch, Mode=OneWay}"
IsHitTestVisible="False"
IsHoldingEnabled="False"
TintColor="{x:Bind WindowViewModel.BackgroundImageTint, Mode=OneWay}"
TintIntensity="{x:Bind WindowViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
Visibility="{x:Bind WindowViewModel.ShowBackgroundImage, Mode=OneWay}" />
</Grid>
</winuiex:WindowEx>

View File

@@ -0,0 +1,728 @@
// 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 CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Foundation;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Accessibility;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
using WinRT.Interop;
using WinUIEx;
namespace Microsoft.CmdPal.UI.Dock;
#pragma warning disable SA1402 // File may only contain a single type
public sealed partial class DockWindow : WindowEx,
IRecipient<BringToTopMessage>,
IRecipient<RequestShowPaletteAtMessage>,
IRecipient<QuitMessage>,
IDisposable
{
#pragma warning disable SA1306 // Field names should begin with lower-case letter
#pragma warning disable SA1310 // Field names should not contain underscore
private readonly uint WM_TASKBAR_RESTART;
#pragma warning restore SA1310 // Field names should not contain underscore
#pragma warning restore SA1306 // Field names should begin with lower-case letter
private readonly IThemeService _themeService;
private readonly DockWindowViewModel _windowViewModel;
private HWND _hwnd = HWND.Null;
private APPBARDATA _appBarData;
private uint _callbackMessageId;
private DockSettings _settings;
private DockViewModel viewModel;
private DockControl _dock;
private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource;
private DockSize _lastSize;
// Store the original WndProc
private WNDPROC? _originalWndProc;
private WNDPROC? _customWndProc;
// internal Settings CurrentSettings => _settings;
public DockWindow()
{
var serviceProvider = App.Current.Services;
var mainSettings = serviceProvider.GetService<SettingsModel>()!;
mainSettings.SettingsChanged += SettingsChangedHandler;
_settings = mainSettings.DockSettings;
_lastSize = _settings.DockSize;
viewModel = serviceProvider.GetService<DockViewModel>()!;
_themeService = serviceProvider.GetRequiredService<IThemeService>();
_themeService.ThemeChanged += ThemeService_ThemeChanged;
_windowViewModel = new DockWindowViewModel(_themeService);
_dock = new DockControl(viewModel);
InitializeComponent();
Root.Children.Add(_dock);
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
AppWindow.IsShownInSwitchers = false;
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
{
overlappedPresenter.SetBorderAndTitleBar(false, false);
overlappedPresenter.IsResizable = false;
}
this.Activated += DockWindow_Activated;
WeakReferenceMessenger.Default.Register<BringToTopMessage>(this);
WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
_hwnd = GetWindowHandle(this);
// Subclass the window to intercept messages
//
// Set up custom window procedure to listen for display changes
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
// member (and instead like, use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our **WindProc will explode**.
_customWndProc = CustomWndProc;
_callbackMessageId = PInvoke.RegisterWindowMessage($"CmdPal_ABM_{_hwnd}");
// TaskbarCreated is the message that's broadcast when explorer.exe
// restarts. We need to know when that happens to be able to bring our
// appbar back
// And this apparently happens on lock screens / hibernates, too
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_customWndProc);
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
// Disable minimize and maximize box
var style = (WINDOW_STYLE)PInvoke.GetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
style &= ~WINDOW_STYLE.WS_MINIMIZEBOX; // Remove WS_MINIMIZEBOX
style &= ~WINDOW_STYLE.WS_MAXIMIZEBOX; // Remove WS_MAXIMIZEBOX
_ = PInvoke.SetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, (int)style);
ShowDesktop.AddHook(this);
UpdateSettings();
}
private void SettingsChangedHandler(SettingsModel sender, object? args)
{
_settings = sender.DockSettings;
UpdateSettings();
}
private void DockWindow_Activated(object sender, WindowActivatedEventArgs args)
{
// These are used for removing the very subtle shadow/border that we get from Windows 11
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
unsafe
{
BOOL value = false;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL));
}
}
private HWND GetWindowHandle(Window window)
{
var hwnd = WindowNative.GetWindowHandle(window);
return new HWND(hwnd);
}
private void UpdateSettings()
{
this.viewModel.UpdateSettings(_settings);
SystemBackdrop = DockSettingsToViews.GetSystemBackdrop(_settings.Backdrop);
// If the backdrop is acrylic, things are more complicated
if (_settings.Backdrop == DockBackdrop.Acrylic)
{
SetAcrylic();
}
_dock.UpdateSettings(_settings);
var side = DockSettingsToViews.GetAppBarEdge(_settings.Side);
if (_appBarData.hWnd != IntPtr.Zero)
{
var sameEdge = _appBarData.uEdge == side;
var sameSize = _lastSize == _settings.DockSize;
if (sameEdge && sameSize)
{
return;
}
DestroyAppBar(_hwnd);
}
CreateAppBar(_hwnd);
}
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
// other Shell surfaces are using, this cannot be set in XAML however.
private void SetAcrylic()
{
if (DesktopAcrylicController.IsSupported())
{
// Hooking up the policy object.
_configurationSource = new SystemBackdropConfiguration
{
// Initial configuration state.
IsInputActive = true,
};
UpdateAcrylic();
}
}
private void UpdateAcrylic()
{
if (_acrylicController != null)
{
_acrylicController.RemoveAllSystemBackdropTargets();
_acrylicController.Dispose();
}
var backdrop = _themeService.CurrentDockTheme.BackdropParameters;
_acrylicController = new DesktopAcrylicController
{
Kind = DesktopAcrylicKind.Thin,
TintColor = backdrop.TintColor,
TintOpacity = backdrop.TintOpacity,
FallbackColor = backdrop.FallbackColor,
LuminosityOpacity = backdrop.LuminosityOpacity,
};
// Enable the system backdrop.
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
}
private void DisposeAcrylic()
{
if (_acrylicController is not null)
{
_acrylicController.Dispose();
_acrylicController = null!;
_configurationSource = null!;
}
}
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
{
DispatcherQueue.TryEnqueue(() =>
{
// We only need to handle acrylic here.
// Transparent background is handled directly in XAML by binding to
// the DockWindowViewModel's ColorizationColor properties.
if (_settings.Backdrop == DockBackdrop.Acrylic)
{
UpdateAcrylic();
}
// ActualTheme / RequestedTheme sync,
// as pilfered from WindowThemeSynchronizer
// LOAD BEARING: Changing the RequestedTheme to Dark then Light then target forces
// a refresh of the theme.
Root.RequestedTheme = ElementTheme.Dark;
Root.RequestedTheme = ElementTheme.Light;
Root.RequestedTheme = _themeService.CurrentDockTheme.Theme;
});
}
private void CreateAppBar(HWND hwnd)
{
_appBarData = new APPBARDATA
{
cbSize = (uint)Marshal.SizeOf<APPBARDATA>(),
hWnd = hwnd,
uCallbackMessage = _callbackMessageId,
};
// Register this window as an appbar
PInvoke.SHAppBarMessage(PInvoke.ABM_NEW, ref _appBarData);
// Stash the last size we created the bar at, so we know when to hot-
// reload it
_lastSize = _settings.DockSize;
UpdateWindowPosition();
}
private void DestroyAppBar(HWND hwnd)
{
PInvoke.SHAppBarMessage(PInvoke.ABM_REMOVE, ref _appBarData);
_appBarData = default;
}
private void UpdateWindowPosition()
{
Logger.LogDebug("UpdateWindowPosition");
var dpi = PInvoke.GetDpiForWindow(_hwnd);
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
// Get system border metrics
var borderWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXBORDER);
var edgeWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXEDGE);
var frameWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXFRAME);
UpdateAppBarDataForEdge(_settings.Side, _settings.DockSize, dpi / 96.0);
// Query and set position
PInvoke.SHAppBarMessage(PInvoke.ABM_QUERYPOS, ref _appBarData);
PInvoke.SHAppBarMessage(PInvoke.ABM_SETPOS, ref _appBarData);
// TODO: investigate ABS_AUTOHIDE and autohide bars.
// I think it's something like this, but I don't totally know
// // _appBarData.lParam = ABS_ALWAYSONTOP;
// _appBarData.lParam = (LPARAM)(int)PInvoke.ABS_AUTOHIDE;
// PInvoke.SHAppBarMessage(ABM_SETSTATE, ref _appBarData);
// PInvoke.SHAppBarMessage(PInvoke.ABM_SETAUTOHIDEBAR, ref _appBarData);
// Account for system borders when moving the window
// Adjust position to account for window frame/border
var adjustedLeft = _appBarData.rc.left - frameWidth;
var adjustedTop = _appBarData.rc.top - frameWidth;
var adjustedWidth = (_appBarData.rc.right - _appBarData.rc.left) + (2 * frameWidth);
var adjustedHeight = (_appBarData.rc.bottom - _appBarData.rc.top) + (2 * frameWidth);
// Move the actual window
PInvoke.MoveWindow(
_hwnd,
adjustedLeft,
adjustedTop,
adjustedWidth,
adjustedHeight,
true);
}
private void UpdateAppBarDataForEdge(DockSide side, DockSize size, double scaleFactor)
{
Logger.LogDebug("UpdateAppBarDataForEdge");
var horizontalHeightDips = DockSettingsToViews.HeightForSize(size);
var verticalWidthDips = DockSettingsToViews.WidthForSize(size);
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
if (side == DockSide.Top)
{
_appBarData.uEdge = PInvoke.ABE_TOP;
_appBarData.rc.left = 0;
_appBarData.rc.top = 0;
_appBarData.rc.right = screenWidth;
_appBarData.rc.bottom = (int)(horizontalHeightDips * scaleFactor);
}
else if (side == DockSide.Bottom)
{
var heightPixels = (int)(horizontalHeightDips * scaleFactor);
_appBarData.uEdge = PInvoke.ABE_BOTTOM;
_appBarData.rc.left = 0;
_appBarData.rc.top = screenHeight - heightPixels;
_appBarData.rc.right = screenWidth;
_appBarData.rc.bottom = screenHeight;
}
else if (side == DockSide.Left)
{
var widthPixels = (int)(verticalWidthDips * scaleFactor);
_appBarData.uEdge = PInvoke.ABE_LEFT;
_appBarData.rc.left = 0;
_appBarData.rc.top = 0;
_appBarData.rc.right = widthPixels;
_appBarData.rc.bottom = screenHeight;
}
else if (side == DockSide.Right)
{
var widthPixels = (int)(verticalWidthDips * scaleFactor);
_appBarData.uEdge = PInvoke.ABE_RIGHT;
_appBarData.rc.left = screenWidth - widthPixels;
_appBarData.rc.top = 0;
_appBarData.rc.right = screenWidth;
_appBarData.rc.bottom = screenHeight;
}
else
{
return;
}
}
private LRESULT CustomWndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
{
// check settings changed
if (msg == PInvoke.WM_SETTINGCHANGE)
{
var isFullscreen = IsWindowFullscreen();
Logger.LogDebug($"WM_SETTINGCHANGE ({isFullscreen})");
if (isFullscreen)
{
this.Hide();
}
else
{
this.Show();
}
if (wParam == (uint)SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETWORKAREA)
{
Logger.LogDebug($"WM_SETTINGCHANGE(SPI_SETWORKAREA)");
// Use debounced call to throttle rapid successive calls
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
}
}
else if (msg == PInvoke.WM_DISPLAYCHANGE)
{
Logger.LogDebug("WM_DISPLAYCHANGE");
// Use dispatcher to ensure we're on the UI thread
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
}
// Intercept WM_SYSCOMMAND to prevent minimize and maximize
else if (msg == PInvoke.WM_SYSCOMMAND)
{
var command = (int)(wParam.Value & 0xFFF0);
if (command == PInvoke.SC_MINIMIZE || command == PInvoke.SC_MAXIMIZE)
{
// Block minimize and maximize commands
return new LRESULT(0);
}
}
// Stop min/max on WM_WINDOWPOSCHANGING too
else if (msg == PInvoke.WM_WINDOWPOSCHANGING)
{
unsafe
{
var pWindowPos = (WINDOWPOS*)lParam.Value;
// Check if the window is being hidden (minimized) or if flags suggest minimize/maximize
if ((pWindowPos->flags & SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW) != 0)
{
// Prevent hiding the window (minimize)
pWindowPos->flags &= ~SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW;
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW;
}
// Additional check: if the window position suggests it's being minimized or maximized
// by checking for dramatic size changes
if (pWindowPos->cx <= 0 || pWindowPos->cy <= 0)
{
// Prevent zero or negative size changes (minimize)
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_NOSIZE;
}
}
}
// Handle WM_SIZE to prevent minimize/maximize state changes
else if (msg == PInvoke.WM_SIZE)
{
var sizeType = (int)wParam.Value;
if (sizeType == PInvoke.SIZE_MINIMIZED || sizeType == PInvoke.SIZE_MAXIMIZED)
{
// Block the size change by not calling the original window procedure
return new LRESULT(0);
}
}
// Handle WM_SHOWWINDOW to prevent hiding (minimize)
else if (msg == PInvoke.WM_SHOWWINDOW)
{
var isBeingShown = wParam.Value != 0;
if (!isBeingShown)
{
// Prevent hiding the window
return new LRESULT(0);
}
}
// Handle double-click on title bar (non-client area)
else if (msg == PInvoke.WM_NCLBUTTONDBLCLK)
{
var hitTest = (int)wParam.Value;
if (hitTest == PInvoke.HTCAPTION)
{
// Block double-click on title bar to prevent maximize
return new LRESULT(0);
}
}
// Handle WM_GETMINMAXINFO to control window size limits
else if (msg == PInvoke.WM_GETMINMAXINFO)
{
// We can modify the min/max tracking info here if needed
// For now, let it pass through but we could restrict max size
}
// Handle the AppBarMessage message
// This is needed to update the position when the work area changes.
// (notably, when the user toggles auto-hide taskbars)
else if (msg == _callbackMessageId)
{
if (wParam.Value == PInvoke.ABN_POSCHANGED)
{
UpdateWindowPosition();
}
}
else if (msg == WM_TASKBAR_RESTART)
{
Logger.LogDebug("WM_TASKBAR_RESTART");
DispatcherQueue.TryEnqueue(() => CreateAppBar(_hwnd));
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(false));
}
// Call the original window procedure for all other messages
return PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
}
void IRecipient<BringToTopMessage>.Receive(BringToTopMessage message)
{
DispatcherQueue.TryEnqueue(() =>
{
var onTop = message.OnTop ? HWND.HWND_TOPMOST : HWND.HWND_NOTOPMOST;
PInvoke.SetWindowPos(_hwnd, onTop, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
PInvoke.SetWindowPos(_hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
});
}
public static bool IsWindowFullscreen()
{
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
if (Marshal.GetExceptionForHR(PInvoke.SHQueryUserNotificationState(out var state)) is null)
{
if (state == QUERY_USER_NOTIFICATION_STATE.QUNS_RUNNING_D3D_FULL_SCREEN ||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_BUSY ||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_PRESENTATION_MODE)
{
return true;
}
}
return false;
}
public void Receive(QuitMessage message)
{
DispatcherQueue.TryEnqueue(() =>
{
DestroyAppBar(_hwnd);
this.Close();
});
}
void IRecipient<RequestShowPaletteAtMessage>.Receive(RequestShowPaletteAtMessage message)
{
DispatcherQueue.TryEnqueue(() => RequestShowPaletteOnUiThread(message.PosDips));
}
private void RequestShowPaletteOnUiThread(Point posDips)
{
// pos is relative to our root. We need to convert to screen coords.
var rootPosDips = Root.TransformToVisual(null).TransformPoint(new Point(0, 0));
var screenPosDips = new Point(rootPosDips.X + posDips.X, rootPosDips.Y + posDips.Y);
var dpi = PInvoke.GetDpiForWindow(_hwnd);
var scaleFactor = dpi / 96.0;
var screenPosPixels = new Point(screenPosDips.X * scaleFactor, screenPosDips.Y * scaleFactor);
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
// Now we're going to find the best position for the palette.
// We want to anchor the palette on the dock side.
// on the top:
// - anchor to the top, left if we're on the left half of the screen
// - anchor to the top, right if we're on the right half of the screen
// On the left:
// - anchor to the top, left if we're on the top half of the screen
// - anchor to the bottom, left if we're on the bottom half of the screen
// On the right:
// - anchor to the top, right if we're on the top half of the screen
// - anchor to the bottom, right if we're on the bottom half of the screen
// On the bottom:
// - anchor to the bottom, left if we're on the left half of the screen
// - anchor to the bottom, right if we're on the right half of the screen
var onTopHalf = screenPosPixels.Y < screenHeight / 2;
var onLeftHalf = screenPosPixels.X < screenWidth / 2;
var onRightHalf = !onLeftHalf;
var onBottomHalf = !onTopHalf;
var anchorPoint = _settings.Side switch
{
DockSide.Top => onLeftHalf ? AnchorPoint.TopLeft : AnchorPoint.TopRight,
DockSide.Bottom => onLeftHalf ? AnchorPoint.BottomLeft : AnchorPoint.BottomRight,
DockSide.Left => onTopHalf ? AnchorPoint.TopLeft : AnchorPoint.BottomLeft,
DockSide.Right => onTopHalf ? AnchorPoint.TopRight : AnchorPoint.BottomRight,
_ => AnchorPoint.TopLeft,
};
// we also need to slide the anchor point a bit away from the dock
var paddingDips = 8;
var paddingPixels = paddingDips * scaleFactor;
PInvoke.GetWindowRect(_hwnd, out var ourRect);
// Depending on the side we're on, we need to offset differently
switch (_settings.Side)
{
case DockSide.Top:
screenPosPixels.Y = ourRect.bottom + paddingPixels;
break;
case DockSide.Bottom:
screenPosPixels.Y = ourRect.top - paddingPixels;
break;
case DockSide.Left:
screenPosPixels.X = ourRect.right + paddingPixels;
break;
case DockSide.Right:
screenPosPixels.X = ourRect.left - paddingPixels;
break;
}
// Now that we know the anchor corner, and where to attempt to place it, we can
// ask the palette to show itself there.
WeakReferenceMessenger.Default.Send<ShowPaletteAtMessage>(new(screenPosPixels, anchorPoint));
}
public DockWindowViewModel WindowViewModel => _windowViewModel;
public void Dispose()
{
DisposeAcrylic();
viewModel.Dispose();
_windowViewModel.Dispose();
}
private void DockWindow_Closed(object sender, WindowEventArgs args)
{
var serviceProvider = App.Current.Services;
var settings = serviceProvider.GetService<SettingsModel>();
settings?.SettingsChanged -= SettingsChangedHandler;
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
DisposeAcrylic();
// Remove our appbar registration
DestroyAppBar(_hwnd);
// Unhook the window procedure
ShowDesktop.RemoveHook();
}
}
// Thank you to https://stackoverflow.com/a/35422795/1481137
internal static class ShowDesktop
{
private const string WORKERW = "WorkerW";
private const string PROGMAN = "Progman";
private static WINEVENTPROC? _hookProc;
private static IntPtr _hookHandle = IntPtr.Zero;
public static void AddHook(Window window)
{
if (IsHooked)
{
return;
}
IsHooked = true;
_hookProc = (WINEVENTPROC)WinEventCallback;
_hookHandle = PInvoke.SetWinEventHook(PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, HMODULE.Null, _hookProc, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT);
}
public static void RemoveHook()
{
if (!IsHooked)
{
return;
}
IsHooked = false;
PInvoke.UnhookWinEvent((HWINEVENTHOOK)_hookHandle);
_hookProc = null;
_hookHandle = IntPtr.Zero;
}
private static string GetWindowClass(HWND hwnd)
{
unsafe
{
fixed (char* c = new char[32])
{
_ = PInvoke.GetClassName(hwnd, (PWSTR)c, 32);
return new string(c);
}
}
}
internal delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
private static void WinEventCallback(
HWINEVENTHOOK hWinEventHook,
uint eventType,
HWND hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime)
{
if (eventType == PInvoke.EVENT_SYSTEM_FOREGROUND)
{
var @class = GetWindowClass(hwnd);
if (string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal))
{
Logger.LogDebug("ShowDesktop invoked. Bring us back");
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(true));
}
}
}
public static bool IsHooked { get; private set; }
}
internal sealed record BringToTopMessage(bool OnTop);
internal sealed record RequestShowPaletteAtMessage(Point PosDips);
internal sealed record ShowPaletteAtMessage(Point PosPixels, AnchorPoint Anchor);
internal enum AnchorPoint
{
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -587,7 +587,7 @@ public sealed partial class ListPage : Page,
var shouldUpdateSelection = false;
// If it's a top level list update we force the reset to the top useful item
if (!sender.IsNested)
if (sender.IsRootPage)
{
shouldUpdateSelection = true;
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -152,7 +152,7 @@ internal sealed partial class TrayIconService
{
if (wParam == PInvoke.WM_USER + 1)
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
}
else if (wParam == PInvoke.WM_USER + 2)
{

View File

@@ -12,6 +12,7 @@ using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
@@ -50,6 +51,7 @@ namespace Microsoft.CmdPal.UI;
public sealed partial class MainWindow : WindowEx,
IRecipient<DismissMessage>,
IRecipient<ShowWindowMessage>,
IRecipient<ShowPaletteAtMessage>,
IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>,
IRecipient<NavigateToPageMessage>,
@@ -134,6 +136,7 @@ public sealed partial class MainWindow : WindowEx,
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
WeakReferenceMessenger.Default.Register<ShowPaletteAtMessage>(this);
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
@@ -332,6 +335,77 @@ public sealed partial class MainWindow : WindowEx,
}
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
{
var positionWindowForTargetMonitor = (HWND hwnd) =>
{
if (target == MonitorBehavior.ToLast)
{
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
else
{
var display = GetScreen(hwnd, target);
PositionCentered(display);
}
};
ShowHwnd(hwndValue, positionWindowForTargetMonitor);
}
private void ShowHwnd(IntPtr hwndValue, Point anchorInPixels, AnchorPoint anchorCorner)
{
var positionWindowForAnchor = (HWND hwnd) =>
{
PInvoke.GetWindowRect(hwnd, out var bounds);
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER;
switch (anchorCorner)
{
case AnchorPoint.TopLeft:
PInvoke.SetWindowPos(
hwnd,
HWND.HWND_TOP,
(int)anchorInPixels.X,
(int)anchorInPixels.Y,
0,
0,
swpFlags);
break;
case AnchorPoint.TopRight:
PInvoke.SetWindowPos(
hwnd,
HWND.HWND_TOP,
(int)(anchorInPixels.X - bounds.Width),
(int)anchorInPixels.Y,
0,
0,
swpFlags);
break;
case AnchorPoint.BottomLeft:
PInvoke.SetWindowPos(
hwnd,
HWND.HWND_TOP,
(int)anchorInPixels.X,
(int)(anchorInPixels.Y - bounds.Height),
0,
0,
swpFlags);
break;
case AnchorPoint.BottomRight:
PInvoke.SetWindowPos(
hwnd,
HWND.HWND_TOP,
(int)(anchorInPixels.X - bounds.Width),
(int)(anchorInPixels.Y - bounds.Height),
0,
0,
swpFlags);
break;
}
};
ShowHwnd(hwndValue, positionWindowForAnchor);
}
private void ShowHwnd(IntPtr hwndValue, Action<HWND>? positionWindow)
{
StopAutoGoHome();
@@ -350,15 +424,9 @@ public sealed partial class MainWindow : WindowEx,
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
}
if (target == MonitorBehavior.ToLast)
if (positionWindow is not null)
{
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
else
{
var display = GetScreen(hwnd, target);
PositionCentered(display);
positionWindow(hwnd);
}
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
@@ -548,6 +616,11 @@ public sealed partial class MainWindow : WindowEx,
ShowHwnd(message.Hwnd, settings.SummonOn);
}
internal void Receive(ShowPaletteAtMessage message)
{
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
}
public void Receive(HideWindowMessage message)
{
// This might come in off the UI thread. Make sure to hop back.
@@ -658,6 +731,8 @@ public sealed partial class MainWindow : WindowEx,
// Sure, it's not ideal, but at least it's not visible.
}
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
// Start auto-go-home timer
RestartAutoGoHome();
}
@@ -1070,6 +1145,7 @@ public sealed partial class MainWindow : WindowEx,
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
Cloak();
this.Hide();
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
return;
}
@@ -1120,6 +1196,8 @@ public sealed partial class MainWindow : WindowEx,
DisposeAcrylic();
}
void IRecipient<ShowPaletteAtMessage>.Receive(ShowPaletteAtMessage message) => Receive(message);
public void Receive(DragStartedMessage message)
{
_preventHideWhenDeactivated = true;

View File

@@ -19,6 +19,9 @@
<Version>$(CmdPalVersion)</Version>
<!-- For MVVM Toolkit Partial Properties/AOT support -->
<LangVersion>preview</LangVersion>
<!-- OutputPath is set in CmdPal.Branding.props -->
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@@ -27,10 +30,10 @@
</PropertyGroup>
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
<!--<PropertyGroup>
<PropertyGroup>
<EnableCmdPalAOT>true</EnableCmdPalAOT>
<GeneratePackageLocally>true</GeneratePackageLocally>
</PropertyGroup>-->
</PropertyGroup>
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
<SelfContained>true</SelfContained>
@@ -74,6 +77,7 @@
<None Remove="Controls\FallbackRankerDialog.xaml" />
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
<None Remove="Controls\ScreenPreview.xaml" />
<None Remove="Controls\ScrollContainer.xaml" />
<None Remove="Controls\SearchBar.xaml" />
<None Remove="IsEnabledTextBlock.xaml" />
<None Remove="ListDetailPage.xaml" />
@@ -84,7 +88,6 @@
<None Remove="SettingsWindow.xaml" />
<None Remove="Settings\AppearancePage.xaml" />
<None Remove="ShellPage.xaml" />
<None Remove="Styles\Colors.xaml" />
<None Remove="Styles\Settings.xaml" />
<None Remove="Styles\TextBox.xaml" />
<None Remove="Styles\Theme.Normal.xaml" />
@@ -140,6 +143,7 @@
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.PerformanceMonitor\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />
@@ -214,6 +218,18 @@
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Dock\DockItemControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ScrollContainer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\CommandPalettePreview.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -264,11 +280,6 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Colors.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Settings.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -59,11 +59,52 @@ GetModuleHandle
GetWindowLong
SetWindowLong
WINDOW_EX_STYLE
CreateWindowEx
WNDCLASSEXW
RegisterClassEx
GetStockObject
GetModuleHandle
MoveWindow
GetSystemMetrics
SHAppBarMessage
ABM_NEW
ABM_QUERYPOS
ABM_SETPOS
ABM_REMOVE
ABM_SETAUTOHIDEBAR
ABS_AUTOHIDE
ABN_POSCHANGED
APPBARDATA
ABE_TOP
ABE_BOTTOM
ABE_LEFT
ABE_RIGHT
SYSTEM_METRICS_INDEX
GetDpiForWindow
SHQueryUserNotificationState
SYSTEM_PARAMETERS_INFO_ACTION
WINDOWPOS
WM_DISPLAYCHANGE
WM_SYSCOMMAND
WM_SETTINGCHANGE
WM_WINDOWPOSCHANGING
WM_SHOWWINDOW
WM_SIZE
WM_GETMINMAXINFO
SetWinEventHook
WINDOW_STYLE
SC_MINIMIZE
SC_MAXIMIZE
SET_WINDOW_POS_FLAGS
SIZE_MAXIMIZED
SIZE_MINIMIZED
HWND_NOTOPMOST
HWND_TOP
HTCAPTION
GetClassName
EVENT_SYSTEM_FOREGROUND
WINEVENT_OUTOFCONTEXT
GetWindowThreadProcessId
AttachThreadInput
AttachThreadInput

View File

@@ -200,14 +200,19 @@
<!-- Back button -->
<StackPanel Orientation="Horizontal">
<!--
This border is to hold a bit of padding we need when
the back button is hidden
-->
<Border Margin="20,0,0,0" Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
<Image
Width="20"
Margin="20,0,6,0"
Margin="0,0,6,0"
HorizontalAlignment="Center"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
AutomationProperties.AccessibilityView="Raw"
Source="ms-appx:///Assets/icon.svg"
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
EasingMode="EaseIn"
@@ -250,7 +255,7 @@
FontSize=14}"
FontSize="16"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
EasingMode="EaseIn"
@@ -297,7 +302,7 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
From="0"

View File

@@ -10,11 +10,13 @@ using CommunityToolkit.WinUI;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.Settings;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
@@ -25,6 +27,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.UI.Core;
using WinUIEx;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using VirtualKey = Windows.System.VirtualKey;
@@ -47,6 +50,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
IRecipient<ShowConfirmationMessage>,
IRecipient<ShowToastMessage>,
IRecipient<NavigateToPageMessage>,
IRecipient<ShowHideDockMessage>,
INotifyPropertyChanged,
IDisposable
{
@@ -64,6 +68,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
private readonly CompositeFormat _pageNavigatedAnnouncement;
private SettingsWindow? _settingsWindow;
private DockWindow? _dockWindow;
private CancellationTokenSource? _focusAfterLoadedCts;
private WeakReference<Page>? _lastNavigatedPageRef;
@@ -94,6 +99,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
@@ -102,6 +109,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
if (App.Current.Services.GetService<SettingsModel>()!.EnableDock)
{
_dockWindow = new DockWindow();
_dockWindow.Show();
}
}
/// <summary>
@@ -248,26 +261,29 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
}
private void InitializeConfirmationDialog(ConfirmResultViewModel vm)
{
vm.SafeInitializePropertiesSynchronous();
}
private void InitializeConfirmationDialog(ConfirmResultViewModel vm) => vm.SafeInitializePropertiesSynchronous();
public void Receive(OpenSettingsMessage message)
{
_ = DispatcherQueue.TryEnqueue(() =>
{
OpenSettings();
OpenSettings(message.Page);
});
}
public void OpenSettings()
public void OpenSettings(string? page = null)
{
if (_settingsWindow is null)
{
_settingsWindow = new SettingsWindow();
}
if (page is not null)
{
_settingsWindow.OpenToPage = page;
_settingsWindow.Navigate(page);
}
_settingsWindow.Activate();
_settingsWindow.BringToFront();
}
@@ -328,10 +344,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch();
public void Receive(HotkeySummonMessage message)
{
_ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
}
public void Receive(HotkeySummonMessage message) => _ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null;
@@ -400,10 +413,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
public void Receive(GoBackMessage message)
{
_ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
}
public void Receive(GoBackMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
private void GoBack(bool withAnimation = true, bool focusSearch = true)
{
@@ -444,10 +454,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
}
public void Receive(GoHomeMessage message)
{
_ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
}
public void Receive(GoHomeMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
private void GoHome(bool withAnimation = true, bool focusSearch = true)
{
@@ -465,6 +472,27 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
}
public void Receive(ShowHideDockMessage message)
{
_ = DispatcherQueue.TryEnqueue(() =>
{
if (message.ShowDock)
{
if (_dockWindow is null)
{
_dockWindow = new DockWindow();
}
_dockWindow.Show();
}
else if (_dockWindow is not null)
{
_dockWindow.Close();
_dockWindow = null;
}
});
}
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
@@ -721,5 +749,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
_focusAfterLoadedCts?.Cancel();
_focusAfterLoadedCts?.Dispose();
_focusAfterLoadedCts = null;
_dockWindow?.Dispose();
}
}

View File

@@ -7,6 +7,7 @@ using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
@@ -36,11 +37,14 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
private bool _isInitialized;
private bool _disposed;
private InternalThemeState _currentState;
private DockThemeSnapshot _currentDockState;
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot;
public DockThemeSnapshot CurrentDockTheme => Volatile.Read(ref _currentDockState);
/// <summary>
/// Initializes the theme service. Must be called after the application window is activated and on UI thread.
/// </summary>
@@ -134,6 +138,60 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
// Atomic swap
Interlocked.Exchange(ref _currentState, newState);
// Compute DockThemeSnapshot from DockSettings
var dockSettings = _settings.DockSettings;
var dockIntensity = Math.Clamp(dockSettings.CustomThemeColorIntensity, 0, 100);
IThemeProvider dockProvider = dockIntensity > 0 && dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
? _colorfulThemeProvider
: _normalThemeProvider;
var dockTint = dockSettings.ColorizationMode switch
{
ColorizationMode.CustomColor => dockSettings.CustomThemeColor,
ColorizationMode.WindowsAccentColor => _uiSettings.GetColorValue(UIColorType.Accent),
ColorizationMode.Image => dockSettings.CustomThemeColor,
_ => Colors.Transparent,
};
var dockEffectiveTheme = GetElementTheme((ElementTheme)dockSettings.Theme);
var dockImageSource = dockSettings.ColorizationMode == ColorizationMode.Image
? LoadImageSafe(dockSettings.BackgroundImagePath)
: null;
var dockStretch = dockSettings.BackgroundImageFit switch
{
BackgroundImageFit.Fill => Stretch.Fill,
_ => Stretch.UniformToFill,
};
var dockOpacity = Math.Clamp(dockSettings.BackgroundImageOpacity, 0, 100) / 100.0;
var dockContext = new ThemeContext
{
Tint = dockTint,
ColorIntensity = dockIntensity,
Theme = dockEffectiveTheme,
BackgroundImageSource = dockImageSource,
BackgroundImageStretch = dockStretch,
BackgroundImageOpacity = dockOpacity,
};
var dockBackdrop = dockProvider.GetAcrylicBackdrop(dockContext);
var dockBlur = dockSettings.BackgroundImageBlurAmount;
var dockBrightness = dockSettings.BackgroundImageBrightness;
var dockSnapshot = new DockThemeSnapshot
{
Tint = dockTint,
TintIntensity = dockIntensity / 100f,
Theme = dockEffectiveTheme,
Backdrop = dockSettings.Backdrop,
BackgroundImageSource = dockImageSource,
BackgroundImageStretch = dockStretch,
BackgroundImageOpacity = dockOpacity,
BackdropParameters = dockBackdrop,
BlurAmount = dockBlur,
BackgroundBrightness = dockBrightness / 100f,
};
Interlocked.Exchange(ref _currentDockState, dockSnapshot);
_resourceSwapper.TryActivateTheme(provider.ThemeKey);
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
}
@@ -205,6 +263,20 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
},
Provider = _normalThemeProvider,
};
_currentDockState = new DockThemeSnapshot
{
Tint = Colors.Transparent,
TintIntensity = 1.0f,
Theme = ElementTheme.Light,
Backdrop = DockBackdrop.Acrylic,
BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
BackgroundImageOpacity = 1,
BackgroundImageSource = null,
BackgroundImageStretch = Stretch.Fill,
BlurAmount = 0,
BackgroundBrightness = 0,
};
}
private void RequestReload()

View File

@@ -0,0 +1,266 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Microsoft.CmdPal.UI.Settings.DockSettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:cpControls="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
xmlns:helpers="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI.Settings"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="1">
<Grid Padding="16">
<StackPanel
MaxWidth="1000"
HorizontalAlignment="Stretch"
Spacing="{StaticResource SettingsCardSpacing}">
<!--
I got these from the samples, but they break XAML hot-reloading,
so I commented them out.
-->
<!--<StackPanel.ChildrenTransitions>
<EntranceThemeTransition FromVerticalOffset="50" />
<RepositionThemeTransition IsStaggeringEnabled="False" />
</StackPanel.ChildrenTransitions>-->
<!-- Enable Dock -->
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xF596;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.EnableDock, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Appearance Section -->
<TextBlock x:Uid="DockAppearanceSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<!-- Dock Size -->
<!-- Dock Position -->
<controls:SettingsExpander
Description="Choose where the dock appears on your screen"
Header="Dock position and appearnce"
IsExpanded="True">
<controls:SettingsExpander.HeaderIcon>
<SymbolIcon Symbol="MoveToFolder" />
</controls:SettingsExpander.HeaderIcon>
<ComboBox
x:Name="DockPositionComboBox"
MinWidth="120"
SelectedIndex="{x:Bind SelectedSideIndex, Mode=TwoWay}">
<ComboBoxItem Content="Left" />
<ComboBoxItem Content="Top" />
<ComboBoxItem Content="Right" />
<ComboBoxItem Content="Bottom" />
</ComboBox>
<controls:SettingsExpander.Items>
<!-- Show Labels -->
<controls:SettingsCard ContentAlignment="Vertical">
<cpControls:CheckBoxWithDescriptionControl
Content="Show labels"
Description="Choose whether to show labels for dock items by default"
IsChecked="{x:Bind ShowLabels, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- Theme Section -->
<TextBlock x:Uid="DockAppearance_ThemeSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="DockAppearance_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE793;}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE770;" />
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_System" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Light_Automation" Tag="Light">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE706;" />
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Light" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark_Automation" Tag="Dark">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE708;" />
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark" />
</StackPanel>
</ComboBoxItem>
</ComboBox>
</controls:SettingsCard>
<!-- Backdrop Style -->
<controls:SettingsCard x:Uid="DockAppearance_Backdrop_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE81E;}">
<ComboBox
x:Name="BackdropComboBox"
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.DockAppearance.BackdropIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="DockAppearance_Backdrop_Transparent" />
<ComboBoxItem x:Uid="DockAppearance_Backdrop_Acrylic" />
</ComboBox>
</controls:SettingsCard>
<!-- Background / Colorization Section -->
<controls:SettingsExpander
x:Uid="DockAppearance_Background_SettingsExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}"
IsExpanded="{x:Bind ViewModel.DockAppearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
<ComboBox
x:Uid="DockAppearance_ColorizationMode"
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.DockAppearance.ColorizationModeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
</ComboBox>
<controls:SettingsExpander.Items>
<!-- none -->
<controls:SettingsCard
x:Uid="DockAppearance_NoBackground_SettingsCard"
HorizontalContentAlignment="Stretch"
ContentAlignment="Vertical"
Visibility="{x:Bind ViewModel.DockAppearance.IsNoBackgroundVisible, Mode=OneWay}">
<TextBlock
x:Uid="DockAppearance_NoBackground_DescriptionTextBlock"
Margin="24"
HorizontalAlignment="Stretch"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
HorizontalTextAlignment="Center"
TextAlignment="Center"
TextWrapping="WrapWholeWords" />
</controls:SettingsCard>
<!-- system accent color -->
<controls:SettingsCard x:Uid="DockAppearance_WindowsAccentColor_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsAccentColorControlsVisible, Mode=OneWay}">
<controls:SettingsCard.Description>
<TextBlock>
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1" />
<Hyperlink
Click="OpenWindowsColorsSettings_Click"
TextDecorations="None"
UnderlineStyle="None">
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_OpenWindowsColorsLinkText" />
</Hyperlink>
</TextBlock>
</controls:SettingsCard.Description>
<controls:SettingsCard.Content>
<Border
MinWidth="32"
MinHeight="32"
CornerRadius="{ThemeResource ControlCornerRadius}">
<Border.Background>
<SolidColorBrush Color="{x:Bind ViewModel.DockAppearance.EffectiveThemeColor, Mode=OneWay}" />
</Border.Background>
</Border>
</controls:SettingsCard.Content>
</controls:SettingsCard>
<!-- background image -->
<controls:SettingsCard
x:Uid="DockAppearance_BackgroundImage_SettingsCard"
Description="{x:Bind ViewModel.DockAppearance.BackgroundImagePath, Mode=OneWay}"
Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="100"
Minimum="-100"
StepFrequency="1"
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBrightness, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="50"
Minimum="0"
StepFrequency="1"
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<ComboBox SelectedIndex="{x:Bind ViewModel.DockAppearance.BackgroundImageFitIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
</ComboBox>
</controls:SettingsCard>
<!-- Background tint color and intensity -->
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTint_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintVisible, Mode=OneWay}">
<cpControls:ColorPickerButton
HasSelectedColor="True"
IsAlphaEnabled="False"
PaletteColors="{x:Bind ViewModel.DockAppearance.Swatches}"
SelectedColor="{x:Bind ViewModel.DockAppearance.ThemeColor, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintIntensityVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="100"
Minimum="1"
StepFrequency="1"
Value="{x:Bind ViewModel.DockAppearance.ColorIntensity, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Reset background image properties -->
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.DockAppearance.ResetBackgroundImagePropertiesCommand}" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- Bands Section -->
<TextBlock x:Uid="DockBandsSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<ItemsRepeater ItemsSource="{x:Bind AllDockBandItems, Mode=OneWay}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="dockVm:DockBandSettingsViewModel">
<controls:SettingsCard
Description="{x:Bind Description, Mode=OneWay}"
Header="{x:Bind Title, Mode=OneWay}"
IsClickEnabled="False">
<controls:SettingsCard.HeaderIcon>
<cpControls:ContentIcon>
<cpControls:ContentIcon.Content>
<cpControls:IconBox
Width="20"
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
</cpControls:ContentIcon.Content>
</cpControls:ContentIcon>
</controls:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{x:Bind IsPinned, Mode=TwoWay}" />
</controls:SettingsCard>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,218 @@
// 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.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.Windows.Storage.Pickers;
namespace Microsoft.CmdPal.UI.Settings;
public sealed partial class DockSettingsPage : Page
{
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
internal SettingsViewModel ViewModel { get; }
public List<DockBandSettingsViewModel> AllDockBandItems => GetAllBandSettings();
public DockSettingsPage()
{
this.InitializeComponent();
var settings = App.Current.Services.GetService<SettingsModel>()!;
var themeService = App.Current.Services.GetService<IThemeService>()!;
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
ViewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
// Initialize UI state
InitializeSettings();
}
private void InitializeSettings()
{
// Initialize UI controls to match current settings
DockPositionComboBox.SelectedIndex = SelectedSideIndex;
BackdropComboBox.SelectedIndex = SelectedBackdropIndex;
}
private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)
{
try
{
if (XamlRoot?.ContentIslandEnvironment is null)
{
return;
}
var windowId = XamlRoot?.ContentIslandEnvironment?.AppWindowId ?? new Microsoft.UI.WindowId(0);
var picker = new FileOpenPicker(windowId)
{
CommitButtonText = ViewModels.Properties.Resources.builtin_settings_appearance_pick_background_image_title!,
SuggestedStartLocation = PickerLocationId.PicturesLibrary,
ViewMode = PickerViewMode.Thumbnail,
};
string[] extensions = [".png", ".bmp", ".jpg", ".jpeg", ".jfif", ".gif", ".tiff", ".tif", ".webp", ".jxr"];
foreach (var ext in extensions)
{
picker.FileTypeFilter!.Add(ext);
}
var file = await picker.PickSingleFileAsync()!;
if (file != null)
{
ViewModel.DockAppearance.BackgroundImagePath = file.Path ?? string.Empty;
}
}
catch (Exception ex)
{
Logger.LogError("Failed to pick background image file for dock", ex);
}
}
private void OpenWindowsColorsSettings_Click(Hyperlink sender, HyperlinkClickEventArgs args)
{
// LOAD BEARING (or BEAR LOADING?): Process.Start with UseShellExecute inside a XAML input event can trigger WinUI reentrancy
// and cause FailFast crashes. Task.Run moves the call off the UI thread to prevent hard process termination.
Task.Run(() =>
{
try
{
_ = Process.Start(new ProcessStartInfo("ms-settings:colors") { UseShellExecute = true });
}
catch (Exception ex)
{
Logger.LogError("Failed to open Windows Settings", ex);
}
});
}
// Property bindings for ComboBoxes
public int SelectedDockSizeIndex
{
get => DockSizeToSelectedIndex(ViewModel.Dock_DockSize);
set => ViewModel.Dock_DockSize = SelectedIndexToDockSize(value);
}
public int SelectedSideIndex
{
get => SideToSelectedIndex(ViewModel.Dock_Side);
set => ViewModel.Dock_Side = SelectedIndexToSide(value);
}
public int SelectedBackdropIndex
{
get => BackdropToSelectedIndex(ViewModel.Dock_Backdrop);
set => ViewModel.Dock_Backdrop = SelectedIndexToBackdrop(value);
}
public bool ShowLabels
{
get => ViewModel.Dock_ShowLabels;
set => ViewModel.Dock_ShowLabels = value;
}
// Conversion methods for ComboBox bindings
private static int DockSizeToSelectedIndex(DockSize size) => size switch
{
DockSize.Small => 0,
DockSize.Medium => 1,
DockSize.Large => 2,
_ => 0,
};
private static DockSize SelectedIndexToDockSize(int index) => index switch
{
0 => DockSize.Small,
1 => DockSize.Medium,
2 => DockSize.Large,
_ => DockSize.Small,
};
private static int SideToSelectedIndex(DockSide side) => side switch
{
DockSide.Left => 0,
DockSide.Top => 1,
DockSide.Right => 2,
DockSide.Bottom => 3,
_ => 1,
};
private static DockSide SelectedIndexToSide(int index) => index switch
{
0 => DockSide.Left,
1 => DockSide.Top,
2 => DockSide.Right,
3 => DockSide.Bottom,
_ => DockSide.Top,
};
private static int BackdropToSelectedIndex(DockBackdrop backdrop) => backdrop switch
{
DockBackdrop.Transparent => 0,
DockBackdrop.Acrylic => 1,
_ => 2,
};
private static DockBackdrop SelectedIndexToBackdrop(int index) => index switch
{
0 => DockBackdrop.Transparent,
1 => DockBackdrop.Acrylic,
_ => DockBackdrop.Acrylic,
};
private List<TopLevelViewModel> GetAllBands()
{
var allBands = new List<TopLevelViewModel>();
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
foreach (var item in tlcManager.DockBands)
{
if (item.IsDockBand)
{
allBands.Add(item);
}
}
return allBands;
}
private List<DockBandSettingsViewModel> GetAllBandSettings()
{
var allSettings = new List<DockBandSettingsViewModel>();
// var allBands = GetAllBands();
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var settingsModel = App.Current.Services.GetService<SettingsModel>()!;
var dockViewModel = App.Current.Services.GetService<DockViewModel>()!;
var allBands = tlcManager.DockBands;
foreach (var band in allBands)
{
var setting = band.DockBandSettings;
if (setting is not null)
{
var bandVm = dockViewModel.FindBandByTopLevel(band);
allSettings.Add(new(
dockSettingsModel: setting,
topLevelAdapter: band,
bandViewModel: bandVm,
settingsModel: settingsModel
));
}
}
return allSettings;
}
}

View File

@@ -72,6 +72,12 @@
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
Icon="{ui:FontIcon Glyph=&#xEA86;}"
Tag="Extensions" />
<!-- xF596 is HolePunchLandscapeTop -->
<NavigationViewItem
x:Name="DockSettingsPageNavItem"
x:Uid="Settings_GeneralPage_NavigationViewItem_Dock"
Icon="{ui:FontIcon Glyph=&#xF596;}"
Tag="Dock" />
</NavigationView.MenuItems>
<Grid>
<Grid.RowDefinitions>

View File

@@ -33,7 +33,9 @@ public sealed partial class SettingsWindow : WindowEx,
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
// Gets or sets optional action invoked after NavigationView is loaded.
public Action NavigationViewLoaded { get; set; } = () => { };
public Action? NavigationViewLoaded { get; set; }
internal string? OpenToPage { get; set; }
public SettingsWindow()
{
@@ -69,7 +71,9 @@ public sealed partial class SettingsWindow : WindowEx,
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
NavView.SelectedItem = NavView.MenuItems[0];
Navigate("General");
Navigate(OpenToPage);
OpenToPage = null;
if (sender is NavigationView navigationView)
{
@@ -96,19 +100,36 @@ public sealed partial class SettingsWindow : WindowEx,
Navigate((selectedItem.Tag as string)!);
}
private void Navigate(string page)
internal void Navigate(string? page)
{
var pageType = page switch
{
null => typeof(GeneralPage),
"General" => typeof(GeneralPage),
"Appearance" => typeof(AppearancePage),
"Extensions" => typeof(ExtensionsPage),
"Dock" => typeof(DockSettingsPage),
_ => null,
};
var actualPage = page ?? "General";
if (pageType is not null)
{
// BreadCrumbs.Clear();
// BreadCrumbs.Add(new(actualPage, actualPage));
NavFrame.Navigate(pageType);
// Now, make sure to actually select the correct menu item too
foreach (var obj in NavView.MenuItems)
{
if (obj is NavigationViewItem item)
{
if (item.Tag is string s && s == page)
{
NavView.SelectedItem = item;
}
}
}
}
}
@@ -261,6 +282,12 @@ public sealed partial class SettingsWindow : WindowEx,
var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage");
BreadCrumbs.Add(new(pageType, pageType));
}
else if (e.SourcePageType == typeof(DockSettingsPage))
{
NavView.SelectedItem = DockSettingsPageNavItem;
var pageType = RS_.GetString("Settings_PageTitles_DockPage");
BreadCrumbs.Add(new(pageType, pageType));
}
else if (e.SourcePageType == typeof(ExtensionPage) && e.Parameter is ProviderSettingsViewModel vm)
{
NavView.SelectedItem = ExtensionPageNavItem;

View File

@@ -389,6 +389,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_NavigationViewItem_Extensions.Content" xml:space="preserve">
<value>Extensions</value>
</data>
<data name="Settings_GeneralPage_NavigationViewItem_Dock.Content" xml:space="preserve">
<value>Dock (Preview)</value>
</data>
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Open Command Palette settings</value>
</data>
@@ -398,6 +401,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="BehaviorSettingsHeader.Text" xml:space="preserve">
<value>Behavior</value>
</data>
<data name="DockAppearanceSettingsHeader.Text" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="DockBandsSettingsHeader.Text" xml:space="preserve">
<value>Bands</value>
</data>
<data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
<value>Search commands...</value>
</data>
@@ -412,6 +421,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
</data>
<data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Description" xml:space="preserve">
<value>Disable animations when switching between pages</value>
</data>
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Header" xml:space="preserve">
<value>Enable Dock</value>
</data>
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Description" xml:space="preserve">
<value>Enable a toolbar with quick access to commands</value>
</data>
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Back</value>
@@ -616,6 +631,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_PageTitles_ExtensionsPage" xml:space="preserve">
<value>Extensions</value>
</data>
<data name="Settings_PageTitles_DockPage" xml:space="preserve">
<value>Dock</value>
</data>
<data name="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack.Content" xml:space="preserve">
<value>Clear search first, then go back</value>
</data>
@@ -727,4 +745,73 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
<value>Version {0}</value>
</data>
<data name="Settings_NavigationViewItem_DockAppearance.Content" xml:space="preserve">
<value>Dock Appearance</value>
</data>
<data name="Settings_PageTitles_DockAppearancePage" xml:space="preserve">
<value>Dock Appearance</value>
</data>
<data name="DockAppearance_ThemeSettingsHeader.Text" xml:space="preserve">
<value>Theme</value>
</data>
<data name="DockAppearance_AppTheme_SettingsCard.Header" xml:space="preserve">
<value>Dock theme mode</value>
</data>
<data name="DockAppearance_AppTheme_SettingsCard.Description" xml:space="preserve">
<value>Select which theme to display for the dock</value>
</data>
<data name="DockAppearance_Backdrop_SettingsCard.Header" xml:space="preserve">
<value>Backdrop style</value>
</data>
<data name="DockAppearance_Backdrop_SettingsCard.Description" xml:space="preserve">
<value>Choose the background effect for the dock</value>
</data>
<data name="DockAppearance_Backdrop_Mica.Content" xml:space="preserve">
<value>Mica</value>
</data>
<data name="DockAppearance_Backdrop_Transparent.Content" xml:space="preserve">
<value>Transparent</value>
</data>
<data name="DockAppearance_Backdrop_Acrylic.Content" xml:space="preserve">
<value>Acrylic</value>
</data>
<data name="DockAppearance_Background_SettingsExpander.Header" xml:space="preserve">
<value>Background</value>
</data>
<data name="DockAppearance_Background_SettingsExpander.Description" xml:space="preserve">
<value>Choose a custom background color or image for the dock</value>
</data>
<data name="DockAppearance_ColorizationMode.Header" xml:space="preserve">
<value>Colorization mode</value>
</data>
<data name="DockAppearance_NoBackground_SettingsCard.Header" xml:space="preserve">
<value>No background</value>
</data>
<data name="DockAppearance_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
<value>No settings</value>
</data>
<data name="DockAppearance_WindowsAccentColor_SettingsCard.Header" xml:space="preserve">
<value>System accent color</value>
</data>
<data name="DockAppearance_BackgroundImage_SettingsCard.Header" xml:space="preserve">
<value>Background image</value>
</data>
<data name="DockAppearance_BackgroundImageBrightness_SettingsCard.Header" xml:space="preserve">
<value>Background image brightness</value>
</data>
<data name="DockAppearance_BackgroundImageBlur_SettingsCard.Header" xml:space="preserve">
<value>Background image blur</value>
</data>
<data name="DockAppearance_BackgroundImageFit_SettingsCard.Header" xml:space="preserve">
<value>Background image fit</value>
</data>
<data name="DockAppearance_BackgroundTint_SettingsCard.Header" xml:space="preserve">
<value>Color tint</value>
</data>
<data name="DockAppearance_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
<value>Color intensity</value>
</data>
<data name="DockAppearance_BackgroundImage_ResetProperties_SettingsCard.Header" xml:space="preserve">
<value>Restore defaults</value>
</data>
</root>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -75,6 +75,8 @@ functionality.
- [Advanced scenarios](#advanced-scenarios)
- [Status messages](#status-messages)
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
- [Class diagram](#class-diagram)
- [Future considerations](#future-considerations)
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
@@ -2045,6 +2047,87 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`,
developers won't have to do anything. The toolkit will just do the right thing
for them.
## Addenda IV: Dock bands
The "dock" is another way to surface commands to the user. This is a
toolbar-like window that can be docked to the side of the screen, or floated as
its own window. It enables another surface for extensions to display real-time
information and shortcuts to users.
Bands are powered by the same interfaces as DevPal itself. Extensions can provide
bands via the new `DockBand` property on `ICommandProvider3`.
```csharp
interface ICommandProvider3 requires ICommandProvider2
{
ICommandItem[] GetDockBands();
};
```
A **Dock Band** is one "strip of items" in the dock. Each band can have multiple
items. This allows an extension to create a strip of buttons that should all be
treated as a single unit. For example, a media player band will want probably
four items:
* one for the previous track
* one for play/pause
* one for next track
* and one to display the album art and track title
`GetDockBands` returns an array of `ICommandItem`s. Each `ICommandItem`
represents one band in the dock. These represent all of the bands that an
extension would allow the user to add to their dock.
All of the `ICommandItem`s returned from `GetDockBands` **must** have a
`Command` with a non-empty `Id` set. If the `Id` is null or empty, DevPal will
ignore that band.
Bands are not automatically added to the dock. Instead, the user must choose
which bands they want to add. This is done via the DevPal settings page.
Furthermore, bands are not displayed in the list of commands in DevPal itself.
This allows extension authors to create objects that are only intended for the
dock, without cluttering up the main DevPal UI, and vice versa.
DevPal will then create UI in the dock for each band the user has chosen to add.
What that looks like will depend on the `Command` in the `ICommandItem`:
* A `IInvokableCommand` will be rendered as a single button. Think "the
time/date" button on the taskbar, that opens the notification center.
* A `IListPage` will be rendered as a strip of buttons, one for each `IListItem`
in the list. Think "media controls" for a music player.
* A `IContentPage` will be rendered as a single button. Clicking that button
will open a flyout with that content rendered in it. Think "weather" or "news"
flyouts.
If the `Command` in the `IListItem`s of a band are pages, then clicking those
buttons will open DevPal to that page, as if it were a flyout from the dock.
The `.Title` property of the top-level `ICommandItem` representing the band will
be used as the name of the band in the settings. So a media player band might
want to set the `Title` to "Contoso Music Player", even if the individual
buttons in the band don't show that title.
Users may also "pin" a top-level command from DevPal into the dock. DevPal will
take care of creating a new band (owned by devpal) with that command in it. This
allows users to add quick shortcuts to their favorite commands in the dock.
Think: pinning an app, or pinning a particular GitHub query.
Bands are added via ID. An extension may choose to have a TopLevelCommand and a
DockBand with the same `Id`. In this case, if the user pins the TopLevelCommand
to the dock, DevPal will pin the band from `GetDockBands`, rather than creating
a simple pinned command. This allows extension authors to seamlessly have a
top-level command present a palette-specific experience, while also having a
dock-specific experience. In our ongoing media player example, the top-level
command might open DevPal to a full-featured music control page, while the dock
band has simpler buttons on it (without a title/subtitle).
Users may choose to have:
* the orientation of the dock: vertical or horizontal
* the size of the dock
* which bands are shown in the dock
* whether the "labels" (read: `Title` & `Subtitle`) of individual bands are
shown or hidden.
- Dock bands will still display the `Title` & `Subtitle` of each item in the
band as the tooltip on those items, even when the "labels" are hidden.
## Class diagram
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -16,7 +16,8 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
public ClipboardHistoryCommandsProvider()
{
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
var page = new ClipboardHistoryListPage(_settingsManager);
_clipboardHistoryListItem = new ListItem(page)
{
Title = Properties.Resources.list_item_title,
Icon = Icons.ClipboardListIcon,
@@ -24,7 +25,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
new CommandContextItem(_settingsManager.Settings.SettingsPage),
],
};
DisplayName = Properties.Resources.provider_display_name;
Icon = Icons.ClipboardListIcon;
Id = "Windows.ClipboardHistory";

View File

@@ -0,0 +1,13 @@
// 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 CoreWidgetProvider.Widgets.Enums;
public enum WidgetDataState
{
Unknown,
Requested, // Request is out, waiting on a response. Current data is stale.
Okay, // Received and updated data, stable state.
Failed, // Failed retrieving data.
}

View File

@@ -0,0 +1,13 @@
// 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 CoreWidgetProvider.Widgets.Enums;
public enum WidgetPageState
{
Unknown,
Configure,
Loading,
Content,
}

View File

@@ -0,0 +1,146 @@
// 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.Diagnostics;
using System.Linq;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class CPUStats : IDisposable
{
// CPU counters
private readonly PerformanceCounter _procPerf = new("Processor Information", "% Processor Utility", "_Total");
private readonly PerformanceCounter _procPerformance = new("Processor Information", "% Processor Performance", "_Total");
private readonly PerformanceCounter _procFrequency = new("Processor Information", "Processor Frequency", "_Total");
private readonly Dictionary<Process, PerformanceCounter> _cpuCounters = new();
internal sealed class ProcessStats
{
public Process? Process { get; set; }
public float CpuUsage { get; set; }
}
public float CpuUsage { get; set; }
public float CpuSpeed { get; set; }
public ProcessStats[] ProcessCPUStats { get; set; }
public List<float> CpuChartValues { get; set; } = new();
public CPUStats()
{
CpuUsage = 0;
ProcessCPUStats =
[
new ProcessStats(),
new ProcessStats(),
new ProcessStats()
];
InitCPUPerfCounters();
}
private void InitCPUPerfCounters()
{
var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0);
foreach (var process in allProcesses)
{
_cpuCounters.Add(process, new PerformanceCounter("Process", "% Processor Time", process.ProcessName, true));
}
}
public void GetData(bool includeTopProcesses)
{
var timer = Stopwatch.StartNew();
CpuUsage = _procPerf.NextValue() / 100;
var usageMs = timer.ElapsedMilliseconds;
CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100);
var speedMs = timer.ElapsedMilliseconds - usageMs;
lock (CpuChartValues)
{
ChartHelper.AddNextChartValue(CpuUsage * 100, CpuChartValues);
}
var chartMs = timer.ElapsedMilliseconds - speedMs;
var processCPUUsages = new Dictionary<Process, float>();
if (includeTopProcesses)
{
foreach (var processCounter in _cpuCounters)
{
try
{
// process might be terminated
processCPUUsages.Add(processCounter.Key, processCounter.Value.NextValue() / Environment.ProcessorCount);
}
catch (InvalidOperationException)
{
// _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters.");
_cpuCounters.Remove(processCounter.Key);
}
catch (Exception)
{
// _log.Error(ex, "Error going through process counters.");
}
}
var cpuIndex = 0;
foreach (var processCPUValue in processCPUUsages.OrderByDescending(x => x.Value).Take(3))
{
ProcessCPUStats[cpuIndex].Process = processCPUValue.Key;
ProcessCPUStats[cpuIndex].CpuUsage = processCPUValue.Value;
cpuIndex++;
}
}
timer.Stop();
var total = timer.ElapsedMilliseconds;
var processesMs = total - chartMs;
// CoreLogger.LogDebug($"[{usageMs}]+[{speedMs}]+[{chartMs}]+[{processesMs}]=[{total}]");
}
internal string CreateCPUImageUrl()
{
return ChartHelper.CreateImageUrl(CpuChartValues, ChartHelper.ChartType.CPU);
}
internal string GetCpuProcessText(int cpuProcessIndex)
{
if (cpuProcessIndex >= ProcessCPUStats.Length)
{
return "no data";
}
return $"{ProcessCPUStats[cpuProcessIndex].Process?.ProcessName} ({ProcessCPUStats[cpuProcessIndex].CpuUsage / 100:p})";
}
internal void KillTopProcess(int cpuProcessIndex)
{
if (cpuProcessIndex >= ProcessCPUStats.Length)
{
return;
}
ProcessCPUStats[cpuProcessIndex].Process?.Kill();
}
public void Dispose()
{
_procPerf.Dispose();
_procPerformance.Dispose();
_procFrequency.Dispose();
foreach (var counter in _cpuCounters.Values)
{
counter.Dispose();
}
}
}

View File

@@ -0,0 +1,289 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Xml.Linq;
namespace CoreWidgetProvider.Helpers;
internal sealed class ChartHelper
{
public enum ChartType
{
CPU,
GPU,
Mem,
Net,
}
public const int ChartHeight = 86;
public const int ChartWidth = 268;
private const string LightGrayBoxStyle = "fill:none;stroke:lightgrey;stroke-width:1";
private const string CPULineStyle = "fill:none;stroke:rgb(57,184,227);stroke-width:1";
private const string GPULineStyle = "fill:none;stroke:rgb(222,104,242);stroke-width:1";
private const string MemLineStyle = "fill:none;stroke:rgb(92,158,250);stroke-width:1";
private const string NetLineStyle = "fill:none;stroke:rgb(245,98,142);stroke-width:1";
private const string FillStyle = "fill:url(#gradientId);stroke:transparent";
private const string CPUBrushStop1Style = "stop-color:rgb(57,184,227);stop-opacity:0.4";
private const string CPUBrushStop2Style = "stop-color:rgb(0,86,110);stop-opacity:0.25";
private const string GPUBrushStop1Style = "stop-color:rgb(222,104,242);stop-opacity:0.4";
private const string GPUBrushStop2Style = "stop-color:rgb(125,0,138);stop-opacity:0.25";
private const string MemBrushStop1Style = "stop-color:rgb(92,158,250);stop-opacity:0.4";
private const string MemBrushStop2Style = "stop-color:rgb(0,34,92);stop-opacity:0.25";
private const string NetBrushStop1Style = "stop-color:rgb(245,98,142);stop-opacity:0.4";
private const string NetBrushStop2Style = "stop-color:rgb(130,0,47);stop-opacity:0.25";
private const string SvgElement = "svg";
private const string RectElement = "rect";
private const string PolylineElement = "polyline";
private const string DefsElement = "defs";
private const string LinearGradientElement = "linearGradient";
private const string StopElement = "stop";
private const string HeightAttr = "height";
private const string WidthAttr = "width";
private const string StyleAttr = "style";
private const string PointsAttr = "points";
private const string OffsetAttr = "offset";
private const string X1Attr = "x1";
private const string X2Attr = "x2";
private const string Y1Attr = "y1";
private const string Y2Attr = "y2";
private const string IdAttr = "id";
private const int MaxChartValues = 34;
public static string CreateImageUrl(List<float> chartValues, ChartType type)
{
var chartStr = CreateChart(chartValues, type);
return "data:image/svg+xml;utf8," + chartStr;
}
/// <summary>
/// Creates an SVG image for the chart.
/// </summary>
/// <param name="chartValues">The values to plot on the chart</param>
/// <param name="type">The type of chart. Each chart type uses different colors.</param>
/// <remarks>
/// The SVG is made of three shapes: <br/>
/// 1. A colored line, plotting the points on the graph <br/>
/// 2. A transparent line, outlining the gradient under the graph <br/>
/// 3. A grey box, outlining the entire image <br/>
/// The SVG also contains a definition for the fill gradient.
/// </remarks>
/// <returns>A string representing the chart as an SVG image.</returns>
public static string CreateChart(List<float> chartValues, ChartType type)
{
// The SVG created by this method will look similar to this:
/*
<svg height="102" width="264">
<defs>
<linearGradient x1="0%" x2="0%" y1="0%" y2="100%" id="gradientId">
<stop offset="0%" style="stop-color:rgb(222,104,242);stop-opacity:0.4" />
<stop offset="95%" style="stop-color:rgb(125,0,138);stop-opacity:0.25" />
</linearGradient>
</defs>
<polyline points="1,91 10,71 253,51 262,31 262,101 1,101" style="fill:url(#gradientId);stroke:transparent" />
<polyline points="1,91 10,71 253,51 262,31" style="fill:none;stroke:rgb(222,104,242);stroke-width:1" />
<rect height="102" width="264" style="fill:none;stroke:lightgrey;stroke-width:1" />
</svg>
*/
// The following code can be uncommented for testing when a static image is desired.
/* chartValues.Clear();
chartValues = new List<float>
{
10, 30, 20, 40, 30, 50, 40, 60, 50, 100,
10, 30, 20, 40, 30, 50, 40, 60, 50, 70,
0, 30, 20, 40, 30, 50, 40, 60, 50, 70,
};*/
var chartDoc = new XDocument();
lock (chartValues)
{
var svgElement = CreateBlankSvg(ChartHeight, ChartWidth);
// Create the line that will show the points on the graph.
var lineElement = new XElement(PolylineElement);
var points = TransformPointsToLine(chartValues, out var startX, out var finalX);
lineElement.SetAttributeValue(PointsAttr, points.ToString());
lineElement.SetAttributeValue(StyleAttr, GetLineStyle(type));
// Create the line that will contain the gradient fill.
TransformPointsToLoop(points, startX, finalX);
var fillElement = new XElement(PolylineElement);
fillElement.SetAttributeValue(PointsAttr, points.ToString());
fillElement.SetAttributeValue(StyleAttr, FillStyle);
// Add the gradient definition and the three shapes to the svg.
svgElement.Add(CreateGradientDefinition(type));
svgElement.Add(fillElement);
svgElement.Add(lineElement);
svgElement.Add(CreateBorderBox(ChartHeight, ChartWidth));
chartDoc.Add(svgElement);
}
return chartDoc.ToString();
}
private static XElement CreateBlankSvg(int height, int width)
{
var svgElement = new XElement(SvgElement);
svgElement.SetAttributeValue(HeightAttr, height);
svgElement.SetAttributeValue(WidthAttr, width);
return svgElement;
}
private static XElement CreateGradientDefinition(ChartType type)
{
var defsElement = new XElement(DefsElement);
var gradientElement = new XElement(LinearGradientElement);
// Vertical gradients are created when x1 and x2 are equal and y1 and y2 differ.
gradientElement.SetAttributeValue(X1Attr, "0%");
gradientElement.SetAttributeValue(X2Attr, "0%");
gradientElement.SetAttributeValue(Y1Attr, "0%");
gradientElement.SetAttributeValue(Y2Attr, "100%");
gradientElement.SetAttributeValue(IdAttr, "gradientId");
string stop1Style;
string stop2Style;
switch (type)
{
case ChartType.GPU:
stop1Style = GPUBrushStop1Style;
stop2Style = GPUBrushStop2Style;
break;
case ChartType.Mem:
stop1Style = MemBrushStop1Style;
stop2Style = MemBrushStop2Style;
break;
case ChartType.Net:
stop1Style = NetBrushStop1Style;
stop2Style = NetBrushStop2Style;
break;
case ChartType.CPU:
default:
stop1Style = CPUBrushStop1Style;
stop2Style = CPUBrushStop2Style;
break;
}
var stop1 = new XElement(StopElement);
stop1.SetAttributeValue(OffsetAttr, "0%");
stop1.SetAttributeValue(StyleAttr, stop1Style);
var stop2 = new XElement(StopElement);
stop2.SetAttributeValue(OffsetAttr, "95%");
stop2.SetAttributeValue(StyleAttr, stop2Style);
gradientElement.Add(stop1);
gradientElement.Add(stop2);
defsElement.Add(gradientElement);
return defsElement;
}
private static XElement CreateBorderBox(int height, int width)
{
var boxElement = new XElement(RectElement);
boxElement.SetAttributeValue(HeightAttr, height);
boxElement.SetAttributeValue(WidthAttr, width);
boxElement.SetAttributeValue(StyleAttr, LightGrayBoxStyle);
return boxElement;
}
private static string GetLineStyle(ChartType type)
{
var lineStyle = type switch
{
ChartType.CPU => CPULineStyle,
ChartType.GPU => GPULineStyle,
ChartType.Mem => MemLineStyle,
ChartType.Net => NetLineStyle,
_ => CPULineStyle,
};
return lineStyle;
}
private static StringBuilder TransformPointsToLine(List<float> chartValues, out int startX, out int finalX)
{
var points = new StringBuilder();
// The X value where the graph starts must be adjusted so that the graph is right-aligned.
// The max available width of the widget is 268. Since there is a 1 px border around the chart, the width of the chart's line must be <=266.
// To create a chart of exactly the right size, we'll have 34 points with 8 pixels in between:
// 1 px left border + 1 px for first point + 33 segments * 8 px per segment + 1 px right border = 267 pixels total in width.
const int pxBetweenPoints = 8;
// When the chart doesn't have all points yet, move the chart over to the right by increasing the starting X coordinate.
// For a chart with only 1 point, the svg will not render a polyline.
// For a chart with 2 points, starting X coordinate == 2 + (34 - 2) * 8 == 1 + 32 * 8 == 1 + 256 == 257
// For a chart with 30 points, starting X coordinate == 2 + (34 - 34) * 8 == 1 + 0 * 8 == 1 + 0 == 2
startX = 2 + ((MaxChartValues - chartValues.Count) * pxBetweenPoints);
finalX = startX;
// Extend graph by one pixel to cover gap on the left when the chart is otherwise full.
if (startX == 2)
{
var invertedHeight = 100 - chartValues[0];
var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
points.Append(CultureInfo.InvariantCulture, $"1,{finalY} ");
}
foreach (var origY in chartValues)
{
// We receive the height as a number up from the X axis (bottom of the chart), but we have to invert it
// since the Y coordinate is relative to the top of the chart.
var invertedHeight = 100 - origY;
// Scale the final Y to whatever the chart height is.
var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
points.Append(CultureInfo.InvariantCulture, $"{finalX},{finalY} ");
finalX += pxBetweenPoints;
}
// Remove the trailing space.
if (points.Length > 0)
{
points.Remove(points.Length - 1, 1);
finalX -= pxBetweenPoints;
}
return points;
}
private static void TransformPointsToLoop(StringBuilder points, int startX, int finalX)
{
// Close the loop.
// Add a point at the most recent X value that corresponds with y = 0
points.Append(CultureInfo.InvariantCulture, $" {finalX},{ChartHeight - 1}");
// Add a point at the start of the chart that corresponds with y = 0
points.Append(CultureInfo.InvariantCulture, $" {startX},{ChartHeight - 1}");
}
public static void AddNextChartValue(float value, List<float> chartValues)
{
if (chartValues.Count >= MaxChartValues)
{
chartValues.RemoveAt(0);
}
chartValues.Add(value);
}
}

View File

@@ -0,0 +1,147 @@
// 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 Timer = System.Timers.Timer;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class DataManager : IDisposable
{
private readonly SystemData _systemData;
private readonly DataType _dataType;
private readonly Timer _updateTimer;
private readonly Action _updateAction;
private const int OneSecondInMilliseconds = 1000;
public DataManager(DataType type, Action updateWidget)
{
_systemData = new SystemData();
_updateAction = updateWidget;
_dataType = type;
_updateTimer = new Timer(OneSecondInMilliseconds);
_updateTimer.Elapsed += UpdateTimer_Elapsed;
_updateTimer.AutoReset = true;
_updateTimer.Enabled = false;
}
private void GetMemoryData()
{
lock (SystemData.MemStats)
{
SystemData.MemStats.GetData();
}
}
private void GetNetworkData()
{
lock (SystemData.NetStats)
{
SystemData.NetStats.GetData();
}
}
private void GetGPUData()
{
lock (SystemData.GPUStats)
{
SystemData.GPUStats.GetData();
}
}
private void GetCPUData(bool includeTopProcesses)
{
lock (SystemData.CpuStats)
{
SystemData.CpuStats.GetData(includeTopProcesses);
}
}
private void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
switch (_dataType)
{
case DataType.CPU:
case DataType.CpuWithTopProcesses:
{
// CPU
GetCPUData(_dataType == DataType.CpuWithTopProcesses);
break;
}
case DataType.GPU:
{
// gpu
GetGPUData();
break;
}
case DataType.Memory:
{
// memory
GetMemoryData();
break;
}
case DataType.Network:
{
// network
GetNetworkData();
break;
}
}
_updateAction?.Invoke();
}
internal MemoryStats GetMemoryStats()
{
lock (SystemData.MemStats)
{
return SystemData.MemStats;
}
}
internal NetworkStats GetNetworkStats()
{
lock (SystemData.NetStats)
{
return SystemData.NetStats;
}
}
internal GPUStats GetGPUStats()
{
lock (SystemData.GPUStats)
{
return SystemData.GPUStats;
}
}
internal CPUStats GetCPUStats()
{
lock (SystemData.CpuStats)
{
return SystemData.CpuStats;
}
}
public void Start()
{
_updateTimer.Start();
}
public void Stop()
{
_updateTimer.Stop();
}
public void Dispose()
{
_systemData.Dispose();
_updateTimer.Dispose();
}
}

View File

@@ -0,0 +1,35 @@
// 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 CoreWidgetProvider.Helpers;
public enum DataType
{
/// <summary>
/// CPU related data.
/// </summary>
CPU,
/// <summary>
/// CPU related data, including the top processes.
/// Calculating the top processes takes a lot longer,
/// so by default we don't.
/// </summary>
CpuWithTopProcesses,
/// <summary>
/// Memory related data.
/// </summary>
Memory,
/// <summary>
/// GPU related data.
/// </summary>
GPU,
/// <summary>
/// Network related data.
/// </summary>
Network,
}

View File

@@ -0,0 +1,283 @@
// 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.Diagnostics;
using System.Globalization;
using System.Linq;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class GPUStats : IDisposable
{
// GPU counters
private readonly Dictionary<int, List<PerformanceCounter>> _gpuCounters = new();
private readonly List<Data> _stats = new();
public sealed class Data
{
public string? Name { get; set; }
public int PhysId { get; set; }
public float Usage { get; set; }
public float Temperature { get; set; }
public List<float> GpuChartValues { get; set; } = new();
}
public GPUStats()
{
GetGPUPerfCounters();
LoadGPUsFromCounters();
}
public void GetGPUPerfCounters()
{
// There are really 4 different things we should be tracking the usage
// of. Similar to how the instance name ends with `3D`, the following
// suffixes are important.
//
// * `3D`
// * `VideoEncode`
// * `VideoDecode`
// * `VideoProcessing`
//
// We could totally put each of those sets of counters into their own
// set. That's what we should do, so that we can report the sum of those
// numbers as the total utilization, and then have them broken out in
// the card template and in the details metadata.
_gpuCounters.Clear();
var pcg = new PerformanceCounterCategory("GPU Engine");
var instanceNames = pcg.GetInstanceNames();
foreach (var instanceName in instanceNames)
{
if (!instanceName.EndsWith("3D", StringComparison.InvariantCulture))
{
continue;
}
var utilizationCounters = pcg.GetCounters(instanceName)
.Where(x => x.CounterName.StartsWith("Utilization Percentage", StringComparison.InvariantCulture));
foreach (var counter in utilizationCounters)
{
var counterKey = counter.InstanceName;
// skip these values
GetKeyValueFromCounterKey("pid", ref counterKey);
GetKeyValueFromCounterKey("luid", ref counterKey);
int phys;
var success = int.TryParse(GetKeyValueFromCounterKey("phys", ref counterKey), out phys);
if (success)
{
GetKeyValueFromCounterKey("eng", ref counterKey);
var engtype = GetKeyValueFromCounterKey("engtype", ref counterKey);
if (engtype != "3D")
{
continue;
}
if (!_gpuCounters.TryGetValue(phys, out var value))
{
value = new();
_gpuCounters.Add(phys, value);
}
value.Add(counter);
}
}
}
}
public void LoadGPUsFromCounters()
{
// The old dev home code tracked GPU stats by querying WMI for the list
// of GPUs, and then matching them up with the performance counter IDs.
//
// We can't use WMI here, because it drags in a dependency on
// Microsoft.Management.Infrastructure, which is not compatible with
// AOT.
//
// For now, we'll just use the indicies as the GPU names.
_stats.Clear();
foreach (var (k, v) in _gpuCounters)
{
var id = k;
var counters = v;
_stats.Add(new Data() { PhysId = id, Name = "GPU " + id });
}
}
public void GetData()
{
foreach (var gpu in _stats)
{
List<PerformanceCounter>? counters;
var success = _gpuCounters.TryGetValue(gpu.PhysId, out counters);
if (success && counters != null)
{
// TODO: This outer try/catch should be replaced with more secure locking around shared resources.
try
{
var sum = 0.0f;
var countersToRemove = new List<PerformanceCounter>();
foreach (var counter in counters)
{
try
{
// NextValue() can throw an InvalidOperationException if the counter is no longer there.
sum += counter.NextValue();
}
catch (InvalidOperationException)
{
// We can't modify the list during the loop, so save it to remove at the end.
// _log.Information(ex, "Failed to get next value, remove");
countersToRemove.Add(counter);
}
catch (Exception)
{
// _log.Error(ex, "Error going through process counters.");
}
}
foreach (var counter in countersToRemove)
{
counters.Remove(counter);
counter.Dispose();
}
gpu.Usage = sum / 100;
lock (gpu.GpuChartValues)
{
ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues);
}
}
catch (Exception)
{
// _log.Error(ex, "Error summing process counters.");
}
}
}
}
internal string CreateGPUImageUrl(int gpuChartIndex)
{
return ChartHelper.CreateImageUrl(_stats.ElementAt(gpuChartIndex).GpuChartValues, ChartHelper.ChartType.GPU);
}
internal string GetGPUName(int gpuActiveIndex)
{
if (_stats.Count <= gpuActiveIndex)
{
return string.Empty;
}
return _stats[gpuActiveIndex].Name ?? string.Empty;
}
internal int GetPrevGPUIndex(int gpuActiveIndex)
{
if (_stats.Count == 0)
{
return 0;
}
if (gpuActiveIndex == 0)
{
return _stats.Count - 1;
}
return gpuActiveIndex - 1;
}
internal int GetNextGPUIndex(int gpuActiveIndex)
{
if (_stats.Count == 0)
{
return 0;
}
if (gpuActiveIndex == _stats.Count - 1)
{
return 0;
}
return gpuActiveIndex + 1;
}
internal float GetGPUUsage(int gpuActiveIndex, string gpuActiveEngType)
{
if (_stats.Count <= gpuActiveIndex)
{
return 0;
}
return _stats[gpuActiveIndex].Usage;
}
internal string GetGPUTemperature(int gpuActiveIndex)
{
// MG Jan 2026: This code was lifted from the old Dev Home codebase.
// However, the performance counters for GPU temperature are not being
// collected. So this function always returns "--" for now.
//
// I have not done the code archeology to figure out why they were
// removed.
if (_stats.Count <= gpuActiveIndex)
{
return "--";
}
var temperature = _stats[gpuActiveIndex].Temperature;
if (temperature == 0)
{
return "--";
}
return temperature.ToString("0.", CultureInfo.InvariantCulture) + " \x00B0C";
}
private string GetKeyValueFromCounterKey(string key, ref string counterKey)
{
if (!counterKey.StartsWith(key, StringComparison.InvariantCulture))
{
return "error";
}
counterKey = counterKey.Substring(key.Length + 1);
if (key.Equals("engtype", StringComparison.Ordinal))
{
return counterKey;
}
var pos = counterKey.IndexOf('_');
if (key.Equals("luid", StringComparison.Ordinal))
{
pos = counterKey.IndexOf('_', pos + 1);
}
var retValue = counterKey.Substring(0, pos);
counterKey = counterKey.Substring(pos + 1);
return retValue;
}
public void Dispose()
{
foreach (var counterPair in _gpuCounters)
{
foreach (var counter in counterPair.Value)
{
counter.Dispose();
}
}
}
}

View File

@@ -0,0 +1,100 @@
// 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.Diagnostics;
using System.Runtime.InteropServices;
using Windows.Win32;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class MemoryStats : IDisposable
{
private readonly PerformanceCounter _memCommitted = new("Memory", "Committed Bytes", string.Empty);
private readonly PerformanceCounter _memCached = new("Memory", "Cache Bytes", string.Empty);
private readonly PerformanceCounter _memCommittedLimit = new("Memory", "Commit Limit", string.Empty);
private readonly PerformanceCounter _memPoolPaged = new("Memory", "Pool Paged Bytes", string.Empty);
private readonly PerformanceCounter _memPoolNonPaged = new("Memory", "Pool Nonpaged Bytes", string.Empty);
public float MemUsage
{
get; set;
}
public ulong AllMem
{
get; set;
}
public ulong UsedMem
{
get; set;
}
public ulong MemCommitted
{
get; set;
}
public ulong MemCommitLimit
{
get; set;
}
public ulong MemCached
{
get; set;
}
public ulong MemPagedPool
{
get; set;
}
public ulong MemNonPagedPool
{
get; set;
}
public List<float> MemChartValues { get; set; } = new();
public void GetData()
{
Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
memStatus.dwLength = (uint)Marshal.SizeOf<Windows.Win32.System.SystemInformation.MEMORYSTATUSEX>();
if (PInvoke.GlobalMemoryStatusEx(ref memStatus))
{
AllMem = memStatus.ullTotalPhys;
var availableMem = memStatus.ullAvailPhys;
UsedMem = AllMem - availableMem;
MemUsage = (float)UsedMem / AllMem;
lock (MemChartValues)
{
ChartHelper.AddNextChartValue(MemUsage * 100, MemChartValues);
}
}
MemCached = (ulong)_memCached.NextValue();
MemCommitted = (ulong)_memCommitted.NextValue();
MemCommitLimit = (ulong)_memCommittedLimit.NextValue();
MemPagedPool = (ulong)_memPoolPaged.NextValue();
MemNonPagedPool = (ulong)_memPoolNonPaged.NextValue();
}
public string CreateMemImageUrl()
{
return ChartHelper.CreateImageUrl(MemChartValues, ChartHelper.ChartType.Mem);
}
public void Dispose()
{
_memCommitted.Dispose();
_memCached.Dispose();
_memCommittedLimit.Dispose();
_memPoolPaged.Dispose();
_memPoolNonPaged.Dispose();
}
}

View File

@@ -0,0 +1,169 @@
// 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.Diagnostics;
using System.Linq;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class NetworkStats : IDisposable
{
private readonly Dictionary<string, List<PerformanceCounter>> _networkCounters = new();
private Dictionary<string, Data> NetworkUsages { get; set; } = new();
private Dictionary<string, List<float>> NetChartValues { get; set; } = new();
public sealed class Data
{
public float Usage
{
get; set;
}
public float Sent
{
get; set;
}
public float Received
{
get; set;
}
}
public NetworkStats()
{
InitNetworkPerfCounters();
}
private void InitNetworkPerfCounters()
{
var pcc = new PerformanceCounterCategory("Network Interface");
var instanceNames = pcc.GetInstanceNames();
foreach (var instanceName in instanceNames)
{
var instanceCounters = new List<PerformanceCounter>();
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName));
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Received/sec", instanceName));
instanceCounters.Add(new PerformanceCounter("Network Interface", "Current Bandwidth", instanceName));
_networkCounters.Add(instanceName, instanceCounters);
NetChartValues.Add(instanceName, new List<float>());
NetworkUsages.Add(instanceName, new Data());
}
}
public void GetData()
{
float maxUsage = 0;
foreach (var networkCounterWithName in _networkCounters)
{
try
{
var sent = networkCounterWithName.Value[0].NextValue();
var received = networkCounterWithName.Value[1].NextValue();
var bandWidth = networkCounterWithName.Value[2].NextValue();
if (bandWidth == 0)
{
continue;
}
var usage = 8 * (sent + received) / bandWidth;
var name = networkCounterWithName.Key;
NetworkUsages[name].Sent = sent;
NetworkUsages[name].Received = received;
NetworkUsages[name].Usage = usage;
var chartValues = NetChartValues[name];
lock (chartValues)
{
ChartHelper.AddNextChartValue(usage * 100, chartValues);
}
if (usage > maxUsage)
{
maxUsage = usage;
}
}
catch (Exception)
{
// Log.Error(ex, "Error getting network data.");
}
}
}
public string CreateNetImageUrl(int netChartIndex)
{
return ChartHelper.CreateImageUrl(NetChartValues.ElementAt(netChartIndex).Value, ChartHelper.ChartType.Net);
}
public string GetNetworkName(int networkIndex)
{
if (NetChartValues.Count <= networkIndex)
{
return string.Empty;
}
return NetChartValues.ElementAt(networkIndex).Key;
}
public Data GetNetworkUsage(int networkIndex)
{
if (NetChartValues.Count <= networkIndex)
{
return new Data();
}
var currNetworkName = NetChartValues.ElementAt(networkIndex).Key;
if (!NetworkUsages.TryGetValue(currNetworkName, out var value))
{
return new Data();
}
return value;
}
public int GetPrevNetworkIndex(int networkIndex)
{
if (NetChartValues.Count == 0)
{
return 0;
}
if (networkIndex == 0)
{
return NetChartValues.Count - 1;
}
return networkIndex - 1;
}
public int GetNextNetworkIndex(int networkIndex)
{
if (NetChartValues.Count == 0)
{
return 0;
}
if (networkIndex == NetChartValues.Count - 1)
{
return 0;
}
return networkIndex + 1;
}
public void Dispose()
{
foreach (var counterPair in _networkCounters)
{
foreach (var counter in counterPair.Value)
{
counter.Dispose();
}
}
}
}

View File

@@ -0,0 +1,142 @@
// 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.Text;
using Microsoft.CmdPal.Core.Common;
namespace CoreWidgetProvider.Helpers;
// This class was pilfered from devhome, but changed much more substantially to
// get the resources out of our resources.pri the way we need.
public static class Resources
{
private static readonly Windows.ApplicationModel.Resources.Core.ResourceMap? _map;
private static readonly string ResourcesPath = "Microsoft.CmdPal.Ext.PerformanceMonitor/Resources";
static Resources()
{
try
{
var currentResourceManager = Windows.ApplicationModel.Resources.Core.ResourceManager.Current;
if (currentResourceManager.MainResourceMap is not null)
{
_map = currentResourceManager.MainResourceMap;
}
}
catch (Exception)
{
// Resource map not available (e.g., during unit tests)
_map = null;
}
}
public static string GetResource(string identifier, ILogger? log = null)
{
if (_map is null)
{
return identifier;
}
var fullKey = $"{ResourcesPath}/{identifier}";
var val = _map.GetValue(fullKey);
#if DEBUG
if (val == null)
{
log?.LogError($"Failed loading resource: {identifier}");
DebugResources(log);
}
#endif
return val!.ValueAsString;
}
public static string ReplaceIdentifersFast(
string original,
ILogger? log = null)
{
// walk the string, looking for a pair of '%' characters
StringBuilder sb = new();
var length = original.Length;
for (var i = 0; i < length; i++)
{
if (original[i] == '%')
{
var end = original.IndexOf('%', i + 1);
if (end > i)
{
var identifier = original.Substring(i + 1, end - i - 1);
var resourceString = GetResource(identifier, log);
sb.Append(resourceString);
i = end; // move index to the end '%'
continue;
}
}
sb.Append(original[i]);
}
return sb.ToString();
}
// These are all the string identifiers that appear in widgets.
public static string[] GetWidgetResourceIdentifiers()
{
return
[
"Widget_Template/Loading",
"Widget_Template_Tooltip/Submit",
"Memory_Widget_Template/SystemMemory",
"Memory_Widget_Template/MemoryUsage",
"Memory_Widget_Template/AllMemory",
"Memory_Widget_Template/UsedMemory",
"Memory_Widget_Template/Committed",
"Memory_Widget_Template/Cached",
"Memory_Widget_Template/NonPagedPool",
"Memory_Widget_Template/PagedPool",
"NetworkUsage_Widget_Template/Network_Usage",
"NetworkUsage_Widget_Template/Sent",
"NetworkUsage_Widget_Template/Received",
"NetworkUsage_Widget_Template/Network_Name",
"NetworkUsage_Widget_Template/Previous_Network",
"NetworkUsage_Widget_Template/Next_Network",
"NetworkUsage_Widget_Template/Ethernet_Heading",
"GPUUsage_Widget_Template/GPU_Usage",
"GPUUsage_Widget_Template/GPU_Name",
"GPUUsage_Widget_Template/GPU_Temperature",
"GPUUsage_Widget_Template/Previous_GPU",
"GPUUsage_Widget_Template/Next_GPU",
"CPUUsage_Widget_Template/CPU_Usage",
"CPUUsage_Widget_Template/CPU_Speed",
"CPUUsage_Widget_Template/Processes",
"CPUUsage_Widget_Template/End_Process",
"Widget_Template_Button/Preview",
"Widget_Template_Button/Save",
"Widget_Template_Button/Cancel",
];
}
private static void DebugResources(ILogger? log)
{
var currentResourceManager = Windows.ApplicationModel.Resources.Core.ResourceManager.Current;
StringBuilder sb = new();
foreach (var (k, v) in currentResourceManager.AllResourceMaps)
{
sb.AppendLine(k);
foreach (var (k2, v2) in v)
{
sb.Append('\t');
sb.AppendLine(k2);
}
sb.AppendLine();
}
log?.LogDebug($"Resource maps:");
log?.LogDebug(sb.ToString());
}
}

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;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class SystemData : IDisposable
{
public static MemoryStats MemStats { get; set; } = new MemoryStats();
public static NetworkStats NetStats { get; set; } = new NetworkStats();
public static GPUStats GPUStats { get; set; } = new GPUStats();
public static CPUStats CpuStats { get; set; } = new CPUStats();
public SystemData()
{
}
public void Dispose()
{
}
}

View File

@@ -0,0 +1,14 @@
The code in this directory was largely lifted from the [DevHome repo].
The specific directory we're using is
https://github.com/microsoft/devhome/tree/main/extensions/CoreWidgetProvider
This has code for all the DevHome performance widgets.
Minimal changes have been made to match our style guidelines.
Additionally, a much larger change was made to Resources.cs, to match our own
resource loading needs.
The code was lifted as of commit d52734ce0e33a82af3313d24c3c2979c37b68bab
[DevHome repo]: https://github.com/microsoft/devhome/

View File

@@ -0,0 +1,20 @@
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5",
"body": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "%Widget_Template/Loading%",
"wrap": true,
"horizontalAlignment": "center"
}
],
"verticalContentAlignment": "center",
"height": "stretch"
}
]
}

View File

@@ -0,0 +1,117 @@
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.6",
"body": [
{
"type": "Input.Text",
"id": "ConfigFile",
"label": "%SSH_Widget_Template/ConfigFilePath%",
"inlineAction": {
"type": "Action.ChooseFile"
},
"spacing": "Medium",
"style": "Url",
"placeholder": "${$root.configuration.currentOrDefaultConfigFile}",
"value": "${$root.configuration.currentOrDefaultConfigFile}"
},
{
"type": "ColumnSet",
"spacing": "Medium",
"horizontalAlignment": "center",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{
"type": "ActionSet",
"actions": [
{
"type": "Action.Execute",
"title": "%Widget_Template_Button/Preview%",
"verb": "CheckPath",
"associatedInputs": "Auto"
}
]
}
]
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"horizontalAlignment": "Center"
}
],
"$when": "${errorMessage != null}",
"separator": true,
"verticalContentAlignment": "Center",
"style": "warning"
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "%SSH_Widget_Template/NumOfHosts%",
"wrap": true,
"spacing": "Medium",
"size": "Small",
"isSubtle": true
},
{
"type": "TextBlock",
"text": "${numOfEntries}",
"wrap": true,
"size": "medium",
"spacing": "None"
}
],
"spacing": "Medium",
"$data": "${$root.configuration}",
"$when": "${$root.hasConfiguration}",
"bleed": true
},
{
"type": "ColumnSet",
"spacing": "Large",
"horizontalAlignment": "center",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{
"type": "Container",
"items": [
{
"type": "ActionSet",
"actions": [
{
"type": "Action.Execute",
"title": "%Widget_Template_Button/Save%",
"verb": "Save",
"isEnabled": "${$root.hasConfiguration}"
},
{
"type": "Action.Execute",
"title": "%Widget_Template_Button/Cancel%",
"verb": "Cancel",
"isEnabled": "${$root.savedConfigFile != \"\"}"
}
]
}
]
}
]
}
]
}
]
}

View File

@@ -0,0 +1,80 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Container",
"$when": "${(count(hosts) == 0)}",
"items": [
{
"type": "TextBlock",
"text": "%SSH_Widget_Template/EmptyHosts%",
"wrap": true,
"weight": "Bolder",
"horizontalAlignment": "Center"
}
],
"spacing": "Medium",
"verticalContentAlignment": "Center"
},
{
"$data": "${hosts}",
"type": "ColumnSet",
"style": "emphasis",
"selectAction": {
"type": "Action.Execute",
"verb": "Connect",
"data": "${host}"
},
"columns": [
{
"type": "Column",
"verticalContentAlignment": "Center",
"width": "auto",
"items": [
{
"type": "Image",
"url": "data:image/png;base64,${icon}",
"size": "small",
"horizontalAlignment": "left"
}
]
},
{
"type": "Column",
"width": "stretch",
"verticalContentAlignment": "Center",
"items": [
{
"type": "TextBlock",
"text": "${host}",
"size": "medium",
"wrap": true,
"horizontalAlignment": "left"
}
]
}
]
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,99 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Image",
"url": "${cpuGraphUrl}",
"height": "${chartHeight}",
"width": "${chartWidth}",
"$when": "${$host.widgetSize != \"small\"}",
"horizontalAlignment": "center"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"isSubtle": true,
"text": "%CPUUsage_Widget_Template/CPU_Usage%"
},
{
"type": "TextBlock",
"size": "large",
"weight": "bolder",
"text": "${cpuUsage}"
}
]
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"isSubtle": true,
"horizontalAlignment": "right",
"text": "%CPUUsage_Widget_Template/CPU_Speed%"
},
{
"type": "TextBlock",
"size": "large",
"horizontalAlignment": "right",
"text": "${cpuSpeed}"
}
]
}
]
},
{
"type": "Container",
"$when": false,
"items": [
{
"type": "TextBlock",
"isSubtle": true,
"text": "%CPUUsage_Widget_Template/Processes%",
"wrap": true
},
{
"type": "TextBlock",
"size": "medium",
"text": "${cpuProc1}"
},
{
"type": "TextBlock",
"size": "medium",
"text": "${cpuProc2}"
},
{
"type": "TextBlock",
"size": "medium",
"text": "${cpuProc3}"
}
]
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,86 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Image",
"url": "${gpuGraphUrl}",
"height": "${chartHeight}",
"width": "${chartWidth}",
"$when": "${$host.widgetSize != \"small\"}",
"horizontalAlignment": "center"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%GPUUsage_Widget_Template/GPU_Usage%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${gpuUsage}",
"type": "TextBlock",
"size": "large",
"weight": "bolder"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%GPUUsage_Widget_Template/GPU_Temperature%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${gpuTemp}",
"type": "TextBlock",
"size": "large",
"weight": "bolder",
"horizontalAlignment": "right"
}
]
}
]
},
{
"text": "%GPUUsage_Widget_Template/GPU_Name%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${gpuName}",
"type": "TextBlock",
"size": "medium"
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,178 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Image",
"url": "${memGraphUrl}",
"height": "${chartHeight}",
"width": "${chartWidth}",
"$when": "${$host.widgetSize != \"small\"}",
"horizontalAlignment": "center"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/UsedMemory%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${usedMem}",
"type": "TextBlock",
"size": "${if($host.widgetSize == \"small\", \"medium\", \"large\")}",
"weight": "bolder"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/AllMemory%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${allMem}",
"type": "TextBlock",
"size": "${if($host.widgetSize == \"small\", \"medium\", \"large\")}",
"weight": "bolder",
"horizontalAlignment": "right"
}
]
}
]
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/Committed%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${committedMem}/${committedLimitMem}",
"type": "TextBlock",
"size": "medium"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/Cached%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${cachedMem}",
"type": "TextBlock",
"size": "medium",
"horizontalAlignment": "right"
}
]
}
]
},
{
"type": "ColumnSet",
"$when": "${$host.widgetSize == \"large\"}",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/PagedPool%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${pagedPoolMem}",
"type": "TextBlock",
"size": "medium"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/NonPagedPool%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${nonPagedPoolMem}",
"type": "TextBlock",
"size": "medium",
"horizontalAlignment": "right"
}
]
}
]
},
{
"type": "ColumnSet",
"$when": "${$host.widgetSize != \"small\"}",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%Memory_Widget_Template/MemoryUsage%",
"type": "TextBlock",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${memUsage}",
"type": "TextBlock",
"size": "medium",
"horizontalAlignment": "right"
}
]
}
]
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,88 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"$when": "${errorMessage != null}",
"items": [
{
"type": "TextBlock",
"text": "${errorMessage}",
"wrap": true,
"size": "small"
}
],
"style": "warning"
},
{
"type": "Container",
"$when": "${errorMessage == null}",
"items": [
{
"type": "Image",
"url": "${netGraphUrl}",
"height": "${chartHeight}",
"width": "${chartWidth}",
"$when": "${$host.widgetSize != \"small\"}",
"horizontalAlignment": "center"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"text": "%NetworkUsage_Widget_Template/Sent%",
"type": "TextBlock",
"spacing": "none",
"size": "small",
"isSubtle": true
},
{
"text": "${netSent}",
"type": "TextBlock",
"size": "large",
"weight": "bolder"
}
]
},
{
"type": "Column",
"items": [
{
"text": "%NetworkUsage_Widget_Template/Received%",
"type": "TextBlock",
"spacing": "none",
"size": "small",
"isSubtle": true,
"horizontalAlignment": "right"
},
{
"text": "${netReceived}",
"type": "TextBlock",
"size": "large",
"weight": "bolder",
"horizontalAlignment": "right"
}
]
}
]
},
{
"text": "%NetworkUsage_Widget_Template/Network_Name%",
"type": "TextBlock",
"size": "small",
"isSubtle": true
},
{
"text": "${networkName}",
"type": "TextBlock",
"size": "medium"
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5"
}

View File

@@ -0,0 +1,31 @@
// 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.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
internal sealed class Icons
{
internal static IconInfo CpuIcon => new("\uE9D9"); // CPU icon
internal static IconInfo MemoryIcon => new("\uE964"); // Memory icon
internal static IconInfo DiskIcon => new("\uE977"); // PC1 icon
internal static IconInfo HardDriveIcon => new("\uEDA2"); // HardDrive icon
internal static IconInfo NetworkIcon => new("\uEC05"); // Network icon
internal static IconInfo StackedAreaIcon => new("\uE9D2"); // StackedArea icon
internal static IconInfo GpuIcon => new("\uE950"); // Component icon
internal static IconInfo NavigateBackwardIcon => new("\uE72B"); // Previous icon
internal static IconInfo NavigateForwardIcon => new("\uE72A"); // Next icon
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.PerformanceMonitor</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.PerformanceMonitor.pri</ProjectPriFileName>
<nullable>enable</nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Diagnostics.PerformanceCounter" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Update="DevHome\Templates\SystemCPUUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemGPUUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemMemoryTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemNetworkUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1 @@
GlobalMemoryStatusEx

View File

@@ -0,0 +1,123 @@
// 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.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// Helper class for creating ListPage's which can listen for when they're
/// loaded and unloaded. This works because CmdPal will attach an event handler
/// to the ItemsChanged event when the page is added to the UI, and remove it
/// when the page is removed from the UI.
///
/// Subclasses should override the Loaded and Unloaded methods to start/stop
/// any background work needed to populate the page.
/// </summary>
internal abstract partial class OnLoadStaticListPage : OnLoadBasePage, IListPage
{
private string _searchText = string.Empty;
public virtual string PlaceholderText { get; set => SetProperty(ref field, value); } = string.Empty;
public virtual string SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
public virtual bool ShowDetails { get; set => SetProperty(ref field, value); }
public virtual bool HasMoreItems { get; set => SetProperty(ref field, value); }
public virtual IFilters? Filters { get; set => SetProperty(ref field, value); }
public virtual IGridProperties? GridProperties { get; set => SetProperty(ref field, value); }
public virtual ICommandItem? EmptyContent { get; set => SetProperty(ref field, value); }
public void LoadMore()
{
}
protected void SetSearchNoUpdate(string newSearchText)
{
_searchText = newSearchText;
}
public abstract IListItem[] GetItems();
}
/// <summary>
/// Helper class for creating ContentPage's which can listen for when they're
/// loaded and unloaded. This works because CmdPal will attach an event handler
/// to the ItemsChanged event when the page is added to the UI, and remove it
/// when the page is removed from the UI.
///
/// Subclasses should override the Loaded and Unloaded methods to start/stop
/// any background work needed to populate the page.
/// </summary>
internal abstract partial class OnLoadContentPage : OnLoadBasePage, IContentPage
{
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
public virtual IContextItem[] Commands { get; set => SetProperty(ref field, value); } = [];
public abstract IContent[] GetContent();
}
internal abstract partial class OnLoadBasePage : Page
{
private int _loadCount;
#pragma warning disable CS0067 // The event is never used
private event TypedEventHandler<object, IItemsChangedEventArgs>? InternalItemsChanged;
#pragma warning restore CS0067 // The event is never used
public event TypedEventHandler<object, IItemsChangedEventArgs> ItemsChanged
{
add
{
InternalItemsChanged += value;
if (_loadCount == 0)
{
Loaded();
}
_loadCount++;
}
remove
{
InternalItemsChanged -= value;
_loadCount--;
_loadCount = Math.Max(0, _loadCount);
if (_loadCount == 0)
{
Unloaded();
}
}
}
protected abstract void Loaded();
protected abstract void Unloaded();
protected void RaiseItemsChanged(int totalItems = -1)
{
try
{
// TODO #181 - This is the same thing that BaseObservable has to deal with.
InternalItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
}
catch
{
}
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,38 @@
// 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.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
public partial class PerformanceMonitorCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly ICommandItem _band;
public PerformanceMonitorCommandsProvider()
{
DisplayName = "Performance Monitor";
Id = "PerformanceMonitor";
Icon = Icons.StackedAreaIcon;
var page = new PerformanceWidgetsPage(false);
var band = new PerformanceWidgetsPage(true);
_band = new CommandItem(band) { Title = DisplayName };
_commands = [
new CommandItem(page) { Title = DisplayName },
];
}
public override ICommandItem[] TopLevelCommands()
{
return _commands;
}
public override ICommandItem[]? GetDockBands()
{
return new ICommandItem[] { _band };
}
}

View File

@@ -0,0 +1,927 @@
// 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json.Nodes;
using CoreWidgetProvider.Helpers;
using CoreWidgetProvider.Widgets.Enums;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// asdfasdf
/// </summary>
/// <remarks>
/// Intentionally, we're using IListPage rather than ListPage. This is so we
/// can get the onload/onunload
/// </remarks>
internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.performanceWidget";
public override string Title => Resources.GetResource("Performance_Monitor_Title");
public override IconInfo Icon => Icons.StackedAreaIcon;
private readonly bool _isBandPage;
private readonly SystemCPUUsageWidgetPage _cpuPage = new();
private readonly ListItem _cpuItem;
private readonly SystemMemoryUsageWidgetPage _memoryPage = new();
private readonly ListItem _memoryItem;
private readonly SystemNetworkUsageWidgetPage _networkPage = new();
private readonly ListItem _networkItem;
private readonly SystemGPUUsageWidgetPage _gpuPage = new();
private readonly ListItem _gpuItem;
// For bands, we want two bands, one for up and one for down
private ListItem? _networkUpItem;
private ListItem? _networkDownItem;
private string _networkUpSpeed = string.Empty;
private string _networkDownSpeed = string.Empty;
public PerformanceWidgetsPage(bool isBandPage = false)
{
_isBandPage = isBandPage;
_cpuItem = new ListItem(_cpuPage)
{
Title = _cpuPage.GetItemTitle(isBandPage),
MoreCommands = _cpuPage.Commands,
};
_cpuPage.Updated += (s, e) =>
{
_cpuItem.Title = _cpuPage.GetItemTitle(isBandPage);
};
_memoryItem = new ListItem(_memoryPage)
{
Title = _memoryPage.GetItemTitle(isBandPage),
MoreCommands = _memoryPage.Commands,
};
_memoryPage.Updated += (s, e) =>
{
_memoryItem.Title = _memoryPage.GetItemTitle(isBandPage);
};
_networkItem = new ListItem(_networkPage)
{
Title = _networkPage.GetItemTitle(isBandPage),
MoreCommands = _networkPage.Commands,
};
_networkPage.Updated += (s, e) =>
{
_networkItem.Title = _networkPage.GetItemTitle(isBandPage);
_networkUpSpeed = _networkPage.GetUpSpeed();
_networkDownSpeed = _networkPage.GetDownSpeed();
_networkDownItem?.Title = $"{_networkDownSpeed}";
_networkUpItem?.Title = $"{_networkUpSpeed}";
};
_gpuItem = new ListItem(_gpuPage)
{
Title = _gpuPage.GetItemTitle(isBandPage),
MoreCommands = _gpuPage.Commands,
};
_gpuPage.Updated += (s, e) =>
{
_gpuItem.Title = _gpuPage.GetItemTitle(isBandPage);
};
if (_isBandPage)
{
// add subtitles to them all
_cpuItem.Subtitle = Resources.GetResource("CPU_Usage_Subtitle");
_memoryItem.Subtitle = Resources.GetResource("Memory_Usage_Subtitle");
_networkItem.Subtitle = Resources.GetResource("Network_Usage_Subtitle");
_gpuItem.Subtitle = Resources.GetResource("GPU_Usage_Subtitle");
}
}
protected override void Loaded()
{
_cpuPage.PushActivate();
_memoryPage.PushActivate();
_networkPage.PushActivate();
_gpuPage.PushActivate();
}
protected override void Unloaded()
{
_cpuPage.PopActivate();
_memoryPage.PopActivate();
_networkPage.PopActivate();
_gpuPage.PopActivate();
}
public override IListItem[] GetItems()
{
if (!_isBandPage)
{
// TODO add details
return new[] { _cpuItem, _memoryItem, _networkItem, _gpuItem };
}
else
{
_networkUpItem = new ListItem(_networkPage)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
MoreCommands = _networkPage.Commands,
};
_networkDownItem = new ListItem(_networkPage)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
MoreCommands = _networkPage.Commands,
};
return new[] { _cpuItem, _memoryItem, _networkDownItem, _networkUpItem, _gpuItem };
}
}
public void Dispose()
{
_cpuPage.Dispose();
_memoryPage.Dispose();
_networkPage.Dispose();
_gpuPage.Dispose();
}
}
/// <summary>
/// Base class for all the performance monitor widget pages.
/// This handles common stuff like loading their widget JSON
/// and updating it when needed.
/// </summary>
internal abstract partial class WidgetPage : OnLoadContentPage
{
internal event EventHandler? Updated;
protected Dictionary<string, string> ContentData { get; } = new();
protected WidgetPageState Page { get; set; } = WidgetPageState.Unknown;
protected Dictionary<WidgetPageState, string> Template { get; set; } = new();
protected JsonObject ContentDataJson
{
get
{
var json = new JsonObject();
lock (ContentData)
{
foreach (var kvp in ContentData)
{
if (kvp.Value is not null)
{
json[kvp.Key] = kvp.Value;
}
}
}
return json;
}
}
private readonly FormContent _formContent = new();
public void UpdateWidget()
{
lock (ContentData)
{
LoadContentData();
}
_formContent.DataJson = ContentDataJson.ToJsonString();
Updated?.Invoke(this, EventArgs.Empty);
}
protected abstract void LoadContentData();
protected abstract string GetTemplatePath(WidgetPageState page);
protected string GetTemplateForPage(WidgetPageState page)
{
if (Template.TryGetValue(page, out var value))
{
CoreLogger.LogDebug($"Using cached template for {page}");
return value;
}
try
{
var path = Path.Combine(Package.Current.EffectivePath, GetTemplatePath(page));
var template = File.ReadAllText(path, Encoding.Default) ?? throw new FileNotFoundException(path);
template = Resources.ReplaceIdentifersFast(template, CoreLogger.Instance);
CoreLogger.LogDebug($"Caching template for {page}");
Template[page] = template;
return template;
}
catch (Exception e)
{
CoreLogger.LogError("Error getting template.", e);
return string.Empty;
}
}
public override IContent[] GetContent()
{
_formContent.TemplateJson = GetTemplateForPage(WidgetPageState.Content);
return [_formContent];
}
/// <summary>
/// Increment our tracker of how many pages have needed us active. This is a
/// little wackier than just OnLoad/Unload. Both the ListPage for
/// PerformanceWidgetsPage itself, AND the widget itself need the stats to
/// be updating. So we use a counter to track how many "clients" need us
/// active. When either is activated, we'll start updating. When both are
/// removed, we'll stop updating.
/// </summary>
internal virtual void PushActivate()
{
_loadCount++;
}
internal virtual void PopActivate()
{
_loadCount--;
}
private int _loadCount;
protected bool IsActive => _loadCount > 0;
protected override void Loaded()
{
PushActivate();
}
protected override void Unloaded()
{
PopActivate();
}
internal static string FloatToPercentString(float value)
{
return ((int)(value * 100)).ToString(CultureInfo.InvariantCulture) + "%";
}
}
internal sealed partial class SystemCPUUsageWidgetPage : WidgetPage, IDisposable
{
public override string Title => Resources.GetResource("CPU_Usage_Title");
public override string Id => "com.microsoft.cmdpal.cpu_widget";
public override IconInfo Icon => Icons.CpuIcon;
private readonly DataManager _dataManager;
public SystemCPUUsageWidgetPage()
{
_dataManager = new(DataType.CPU, () => UpdateWidget());
Commands = [
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting CPU stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetCPUStats();
var dataDuration = timer.ElapsedMilliseconds;
ContentData["cpuUsage"] = FloatToPercentString(currentData.CpuUsage);
ContentData["cpuSpeed"] = SpeedToString(currentData.CpuSpeed);
ContentData["cpuGraphUrl"] = currentData.CreateCPUImageUrl();
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
// ContentData["cpuProc1"] = currentData.GetCpuProcessText(0);
// ContentData["cpuProc2"] = currentData.GetCpuProcessText(1);
// ContentData["cpuProc3"] = currentData.GetCpuProcessText(2);
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"CPU stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
// DataState = WidgetDataState.Okay;
}
catch (Exception e)
{
// Log.Error(e, "Error retrieving stats.");
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
// ContentData = content.ToJsonString();
// DataState = WidgetDataState.Failed;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemCPUUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemCPUUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("cpuUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("CPU_Usage_Label"), usage);
}
else
{
return isBandPage ? Resources.GetResource("CPU_Usage_Unknown") : Resources.GetResource("CPU_Usage_Unknown_Label");
}
}
private string SpeedToString(float cpuSpeed)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.00} GHz", cpuSpeed / 1000);
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
public void Dispose()
{
_dataManager.Dispose();
}
}
internal sealed partial class SystemMemoryUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.memory_widget";
public override string Title => Resources.GetResource("Memory_Usage_Title");
public override IconInfo Icon => Icons.MemoryIcon;
private readonly DataManager _dataManager;
public SystemMemoryUsageWidgetPage()
{
_dataManager = new(DataType.Memory, () => UpdateWidget());
Commands = [
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting Memory stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetMemoryStats();
var dataDuration = timer.ElapsedMilliseconds;
ContentData["allMem"] = MemUlongToString(currentData.AllMem);
ContentData["usedMem"] = MemUlongToString(currentData.UsedMem);
ContentData["memUsage"] = FloatToPercentString(currentData.MemUsage);
ContentData["committedMem"] = MemUlongToString(currentData.MemCommitted);
ContentData["committedLimitMem"] = MemUlongToString(currentData.MemCommitLimit);
ContentData["cachedMem"] = MemUlongToString(currentData.MemCached);
ContentData["pagedPoolMem"] = MemUlongToString(currentData.MemPagedPool);
ContentData["nonPagedPoolMem"] = MemUlongToString(currentData.MemNonPagedPool);
ContentData["memGraphUrl"] = currentData.CreateMemImageUrl();
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"Memory stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemMemoryTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemMemoryTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("memUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Memory_Usage_Label"), usage);
}
else
{
return isBandPage ? Resources.GetResource("Memory_Usage_Unknown") : Resources.GetResource("Memory_Usage_Unknown_Label");
}
}
private string MemUlongToString(ulong memBytes)
{
if (memBytes < 1024)
{
return memBytes.ToString(CultureInfo.InvariantCulture) + " B";
}
var memSize = memBytes / 1024.0;
if (memSize < 1024)
{
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " kB";
}
memSize /= 1024;
if (memSize < 1024)
{
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " MB";
}
memSize /= 1024;
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " GB";
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
public void Dispose()
{
_dataManager.Dispose();
}
}
internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.network_widget";
public override string Title => Resources.GetResource("Network_Usage_Title");
public override IconInfo Icon => Icons.NetworkIcon;
private readonly DataManager _dataManager;
private int _networkIndex;
public SystemNetworkUsageWidgetPage()
{
_dataManager = new(DataType.Network, () => UpdateWidget());
Commands = [
new CommandContextItem(new PrevNetworkCommand(this) { Name = Resources.GetResource("Previous_Network_Title") }),
new CommandContextItem(new NextNetworkCommand(this) { Name = Resources.GetResource("Next_Network_Title") }),
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting Network stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetNetworkStats();
var dataDuration = timer.ElapsedMilliseconds;
var netName = currentData.GetNetworkName(_networkIndex);
var networkStats = currentData.GetNetworkUsage(_networkIndex);
ContentData["networkUsage"] = FloatToPercentString(networkStats.Usage);
ContentData["netSent"] = BytesToBitsPerSecString(networkStats.Sent);
ContentData["netReceived"] = BytesToBitsPerSecString(networkStats.Received);
ContentData["networkName"] = netName;
ContentData["netGraphUrl"] = currentData.CreateNetImageUrl(_networkIndex);
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"Network stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemNetworkUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemNetworkUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("networkName", out var name) && ContentData.TryGetValue("networkUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Network_Usage_Label"), name, usage);
}
else
{
return isBandPage ? Resources.GetResource("Network_Usage_Unknown") : Resources.GetResource("Network_Usage_Unknown_Label");
}
}
// up/down speed is always used for bands
public string GetUpSpeed()
{
if (ContentData.TryGetValue("netSent", out var upSpeed))
{
return upSpeed;
}
else
{
return "???";
}
}
public string GetDownSpeed()
{
if (ContentData.TryGetValue("netReceived", out var downSpeed))
{
return downSpeed;
}
else
{
return "???";
}
}
private string BytesToBitsPerSecString(float value)
{
// Bytes to bits
value *= 8;
// bits to Kbits
value /= 1024;
if (value < 1024)
{
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Kbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Kbps", value);
}
// Kbits to Mbits
value /= 1024;
if (value < 1024)
{
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Mbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Mbps", value);
}
// Mbits to Gbits
value /= 1024;
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Gbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Gbps", value);
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
private void HandlePrevNetwork()
{
_networkIndex = _dataManager.GetNetworkStats().GetPrevNetworkIndex(_networkIndex);
UpdateWidget();
}
private void HandleNextNetwork()
{
_networkIndex = _dataManager.GetNetworkStats().GetNextNetworkIndex(_networkIndex);
UpdateWidget();
}
public void Dispose()
{
_dataManager.Dispose();
}
private sealed partial class PrevNetworkCommand : InvokableCommand
{
private readonly SystemNetworkUsageWidgetPage _page;
public PrevNetworkCommand(SystemNetworkUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.network_widget.prev";
public override IconInfo Icon => Icons.NavigateBackwardIcon;
public override ICommandResult Invoke()
{
_page.HandlePrevNetwork();
return CommandResult.KeepOpen();
}
}
private sealed partial class NextNetworkCommand : InvokableCommand
{
private readonly SystemNetworkUsageWidgetPage _page;
public NextNetworkCommand(SystemNetworkUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.network_widget.next";
public override IconInfo Icon => Icons.NavigateForwardIcon;
public override ICommandResult Invoke()
{
_page.HandleNextNetwork();
return CommandResult.KeepOpen();
}
}
}
internal sealed partial class SystemGPUUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.gpu_widget";
public override string Title => Resources.GetResource("GPU_Usage_Title");
public override IconInfo Icon => Icons.GpuIcon;
private readonly DataManager _dataManager;
private readonly string _gpuActiveEngType = "3D";
private int _gpuActiveIndex;
public SystemGPUUsageWidgetPage()
{
_dataManager = new(DataType.GPU, () => UpdateWidget());
Commands = [
new CommandContextItem(new PrevGPUCommand(this) { Name = Resources.GetResource("Previous_GPU_Title") }),
new CommandContextItem(new NextGPUCommand(this) { Name = Resources.GetResource("Next_GPU_Title") }),
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting GPU stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var stats = _dataManager.GetGPUStats();
var dataDuration = timer.ElapsedMilliseconds;
var gpuName = stats.GetGPUName(_gpuActiveIndex);
ContentData["gpuUsage"] = FloatToPercentString(stats.GetGPUUsage(_gpuActiveIndex, _gpuActiveEngType));
ContentData["gpuName"] = gpuName;
ContentData["gpuTemp"] = stats.GetGPUTemperature(_gpuActiveIndex);
ContentData["gpuGraphUrl"] = stats.CreateGPUImageUrl(_gpuActiveIndex);
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"GPU stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemGPUUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemGPUUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("gpuName", out var name) && ContentData.TryGetValue("gpuUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("GPU_Usage_Label"), name, usage);
}
else
{
return isBandPage ? Resources.GetResource("GPU_Usage_Unknown") : Resources.GetResource("GPU_Usage_Unknown_Label");
}
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
private void HandlePrevGPU()
{
_gpuActiveIndex = _dataManager.GetGPUStats().GetPrevGPUIndex(_gpuActiveIndex);
UpdateWidget();
}
private void HandleNextGPU()
{
_gpuActiveIndex = _dataManager.GetGPUStats().GetNextGPUIndex(_gpuActiveIndex);
UpdateWidget();
}
public void Dispose()
{
_dataManager.Dispose();
}
private sealed partial class PrevGPUCommand : InvokableCommand
{
private readonly SystemGPUUsageWidgetPage _page;
public PrevGPUCommand(SystemGPUUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.gpu_widget.prev";
public override IconInfo Icon => Icons.NavigateBackwardIcon;
public override ICommandResult Invoke()
{
_page.HandlePrevGPU();
return CommandResult.KeepOpen();
}
}
private sealed partial class NextGPUCommand : InvokableCommand
{
private readonly SystemGPUUsageWidgetPage _page;
public NextGPUCommand(SystemGPUUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.gpu_widget.next";
public override IconInfo Icon => Icons.NavigateForwardIcon;
public override ICommandResult Invoke()
{
_page.HandleNextGPU();
return CommandResult.KeepOpen();
}
}
}
internal sealed partial class OpenTaskManagerCommand : InvokableCommand
{
internal static readonly OpenTaskManagerCommand Instance = new();
public override string Id => "com.microsoft.cmdpal.open_task_manager";
public override IconInfo Icon => Icons.StackedAreaIcon; // StackedAreaIcon looks like task manager's icon
public override string Name => Resources.GetResource("Open_Task_Manager_Title");
public override ICommandResult Invoke()
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = "taskmgr.exe",
UseShellExecute = true,
});
}
catch (Exception e)
{
CoreLogger.LogError("Error launching Task Manager.", e);
}
return CommandResult.Hide();
}
}

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