mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-03 17:10:14 +02:00
Compare commits
54 Commits
dev/snickl
...
copilot/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
018f9417a1 | ||
|
|
658ddead43 | ||
|
|
7051b8939b | ||
|
|
0d41d45a64 | ||
|
|
86115a54f6 | ||
|
|
99706d4324 | ||
|
|
ff194c0b5f | ||
|
|
a151d6c8b6 | ||
|
|
7610b77109 | ||
|
|
549b32e273 | ||
|
|
0ccf5986e9 | ||
|
|
87b24afa23 | ||
|
|
74c53c14e6 | ||
|
|
77173cd075 | ||
|
|
149e7b1efe | ||
|
|
0c2d24c3f6 | ||
|
|
b81ea23c68 | ||
|
|
39bbf0593e | ||
|
|
4620f6f381 | ||
|
|
da3b12d536 | ||
|
|
bab77edd11 | ||
|
|
414ee86fb3 | ||
|
|
eeeb6c0c93 | ||
|
|
70e082ce4f | ||
|
|
8404bfbebb | ||
|
|
77412d1961 | ||
|
|
fad5a3ac69 | ||
|
|
52cab7058a | ||
|
|
35a3c55f29 | ||
|
|
ed16ae7b2a | ||
|
|
f049cc5839 | ||
|
|
f82fb2a411 | ||
|
|
90131e35d9 | ||
|
|
77355ef2fb | ||
|
|
a130969d0a | ||
|
|
d1605640ca | ||
|
|
9859fb6196 | ||
|
|
3bd85efc56 | ||
|
|
f8453214fb | ||
|
|
0aca7c292c | ||
|
|
c6f1a09fa2 | ||
|
|
b72224ea0b | ||
|
|
e323da939b | ||
|
|
75fb296bb2 | ||
|
|
3d69785ca4 | ||
|
|
f6b0996c9b | ||
|
|
748d5e485c | ||
|
|
1718cecedb | ||
|
|
4f0c8f476a | ||
|
|
a953a39aec | ||
|
|
8c4ff37a50 | ||
|
|
02062dd023 | ||
|
|
bcbca0d5dd | ||
|
|
f0134e4448 |
9
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
9
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -40,7 +40,6 @@ body:
|
||||
- Other (please specify in "Steps to Reproduce")
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Area(s) with issue?
|
||||
@@ -106,7 +105,13 @@ body:
|
||||
placeholder: What happened instead?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: upload
|
||||
id: bugreportfile
|
||||
attributes:
|
||||
label: Upload Bug Report ZIP-file
|
||||
description: Right-clicking the PowerToys tray icon in the taskbar and selecting “Report bug” generates a ZIP file containing diagnostic information about your setup and PowerToys logs, helping us better understand and troubleshoot the issue.
|
||||
validations:
|
||||
required: false
|
||||
- id: additionalInfo
|
||||
type: textarea
|
||||
attributes:
|
||||
|
||||
5
.github/actions/spell-check/allow/code.txt
vendored
5
.github/actions/spell-check/allow/code.txt
vendored
@@ -1,6 +1,7 @@
|
||||
# COLORS
|
||||
|
||||
argb
|
||||
Bgr
|
||||
bgra
|
||||
BLACKONWHITE
|
||||
BLUEGRAY
|
||||
@@ -28,6 +29,7 @@ RUS
|
||||
|
||||
AYUV
|
||||
bak
|
||||
HDP
|
||||
Bcl
|
||||
bgcode
|
||||
Deflatealgorithm
|
||||
@@ -46,6 +48,7 @@ nupkg
|
||||
petabyte
|
||||
resw
|
||||
resx
|
||||
runtimeconfig
|
||||
srt
|
||||
Stereolithography
|
||||
terabyte
|
||||
@@ -297,6 +300,8 @@ pwa
|
||||
|
||||
AOT
|
||||
Aot
|
||||
ify
|
||||
TFM
|
||||
|
||||
# YML
|
||||
onefuzz
|
||||
|
||||
73
.github/actions/spell-check/expect.txt
vendored
73
.github/actions/spell-check/expect.txt
vendored
@@ -16,6 +16,7 @@ adaptivecards
|
||||
ADDSTRING
|
||||
ADDUNDORECORD
|
||||
ADifferent
|
||||
ADMINS
|
||||
adml
|
||||
admx
|
||||
advfirewall
|
||||
@@ -129,6 +130,7 @@ bthprops
|
||||
bti
|
||||
BTNFACE
|
||||
bugreport
|
||||
bugreportfile
|
||||
BUILDARCH
|
||||
BUILDNUMBER
|
||||
buildtransitive
|
||||
@@ -168,7 +170,11 @@ cim
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
classguid
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
claude
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
clientside
|
||||
@@ -200,6 +206,7 @@ colorformat
|
||||
colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -217,6 +224,8 @@ CONTEXTHELP
|
||||
CONTEXTMENUHANDLER
|
||||
contractversion
|
||||
CONTROLPARENT
|
||||
Convs
|
||||
cooldown
|
||||
copiedcolorrepresentation
|
||||
COPYPEN
|
||||
COREWINDOW
|
||||
@@ -227,6 +236,8 @@ cpcontrols
|
||||
cph
|
||||
cplusplus
|
||||
CPower
|
||||
cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
createdump
|
||||
CREATEPROCESS
|
||||
@@ -249,6 +260,8 @@ CTLCOLORSTATIC
|
||||
CURRENTDIR
|
||||
CURSORINFO
|
||||
cursorpos
|
||||
CURSORSHOWING
|
||||
cursorwrap
|
||||
customaction
|
||||
CUSTOMACTIONTEST
|
||||
CVal
|
||||
@@ -265,12 +278,14 @@ dacl
|
||||
datareader
|
||||
datatracker
|
||||
Dayof
|
||||
dbcc
|
||||
DBID
|
||||
DBLCLKS
|
||||
DBLEPSILON
|
||||
DBPROP
|
||||
DBPROPIDSET
|
||||
DBPROPSET
|
||||
DBT
|
||||
DCBA
|
||||
DCOM
|
||||
DComposition
|
||||
@@ -286,6 +301,8 @@ DEFAULTFLAGS
|
||||
DEFAULTICON
|
||||
defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTSIZE
|
||||
defaulttonearest
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
@@ -305,11 +322,21 @@ DESKTOPABSOLUTEPARSING
|
||||
desktopshorcutinstalled
|
||||
devblogs
|
||||
devdocs
|
||||
devenv
|
||||
DEVICEINTERFACE
|
||||
devicetype
|
||||
DEVINTERFACE
|
||||
devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
DEVNODES
|
||||
devpal
|
||||
DEVTYP
|
||||
dfx
|
||||
DIALOGEX
|
||||
diffs
|
||||
digicert
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
diskmgmt
|
||||
@@ -423,6 +450,12 @@ eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fdw
|
||||
fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
FILEFLAGS
|
||||
FILEFLAGSMASK
|
||||
@@ -439,6 +472,7 @@ FILESYSPATH
|
||||
Filetime
|
||||
FILEVERSION
|
||||
FILTERMODE
|
||||
FInc
|
||||
findfast
|
||||
FIXEDFILEINFO
|
||||
FIXEDSYS
|
||||
@@ -494,6 +528,7 @@ GPOCA
|
||||
gpp
|
||||
gpu
|
||||
gradians
|
||||
GRGX
|
||||
GSM
|
||||
gtm
|
||||
guiddata
|
||||
@@ -524,11 +559,13 @@ HCRYPTPROV
|
||||
hcursor
|
||||
hcwhite
|
||||
hdc
|
||||
HDEVNOTIFY
|
||||
hdr
|
||||
hdrop
|
||||
hdwwiz
|
||||
Helpline
|
||||
helptext
|
||||
hgdiobj
|
||||
HGFE
|
||||
hglobal
|
||||
hhk
|
||||
@@ -673,12 +710,12 @@ jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
jjw
|
||||
jobject
|
||||
JOBOBJECT
|
||||
jpe
|
||||
jpnime
|
||||
Jsons
|
||||
jsonval
|
||||
jxr
|
||||
kbmcontrols
|
||||
keybd
|
||||
KEYBDDATA
|
||||
KEYBDINPUT
|
||||
@@ -707,6 +744,7 @@ Ldone
|
||||
Ldr
|
||||
LEFTSCROLLBAR
|
||||
LEFTTEXT
|
||||
leftclick
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
@@ -738,6 +776,8 @@ lowlevel
|
||||
LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
lpch
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
lpcmi
|
||||
@@ -755,6 +795,7 @@ LPMONITORINFO
|
||||
LPOSVERSIONINFOEXW
|
||||
LPQUERY
|
||||
lprc
|
||||
LPrivate
|
||||
LPSAFEARRAY
|
||||
lpstr
|
||||
lpsz
|
||||
@@ -796,10 +837,13 @@ MAPPEDTOSAMEKEY
|
||||
MAPTOSAMESHORTCUT
|
||||
MAPVK
|
||||
MARKDOWNPREVIEWHANDLERCPP
|
||||
MAXDWORD
|
||||
MAXSHORTCUTSIZE
|
||||
maxversiontested
|
||||
MBM
|
||||
MBR
|
||||
Mbuttondown
|
||||
mcp
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
@@ -811,11 +855,13 @@ MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metacharacter
|
||||
metadatamatters
|
||||
Metadatas
|
||||
Metacharacter
|
||||
metafile
|
||||
metapackage
|
||||
mfc
|
||||
mfalse
|
||||
Mgmt
|
||||
Microwaved
|
||||
midl
|
||||
@@ -838,6 +884,7 @@ mmsys
|
||||
mobileredirect
|
||||
mockapi
|
||||
MODALFRAME
|
||||
modelcontextprotocol
|
||||
MODESPRUNED
|
||||
MONITORENUMPROC
|
||||
MONITORINFO
|
||||
@@ -872,9 +919,10 @@ MSLLHOOKSTRUCT
|
||||
Mso
|
||||
msrc
|
||||
msstore
|
||||
mstsc
|
||||
msvcp
|
||||
MT
|
||||
MTND
|
||||
mtrue
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
muxc
|
||||
@@ -882,6 +930,8 @@ mvvm
|
||||
MVVMTK
|
||||
MWBEx
|
||||
MYICON
|
||||
myorg
|
||||
myrepo
|
||||
NAMECHANGE
|
||||
namespaceanddescendants
|
||||
nao
|
||||
@@ -996,6 +1046,8 @@ OEMCONVERT
|
||||
officehubintl
|
||||
OFN
|
||||
ofs
|
||||
OICI
|
||||
OICIIO
|
||||
oldcolor
|
||||
olditem
|
||||
oldpath
|
||||
@@ -1006,6 +1058,7 @@ openas
|
||||
opencode
|
||||
OPENFILENAME
|
||||
opensource
|
||||
openurl
|
||||
openxmlformats
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
@@ -1028,6 +1081,7 @@ Packagemanager
|
||||
PACL
|
||||
padx
|
||||
pady
|
||||
PAI
|
||||
PAINTSTRUCT
|
||||
PALETTEWINDOW
|
||||
PARENTNOTIFY
|
||||
@@ -1200,6 +1254,7 @@ RAWPATH
|
||||
rbhid
|
||||
rclsid
|
||||
RCZOOMIT
|
||||
rdp
|
||||
RDW
|
||||
READMODE
|
||||
READOBJECTS
|
||||
@@ -1227,6 +1282,7 @@ remappings
|
||||
REMAPSUCCESSFUL
|
||||
REMAPUNSUCCESSFUL
|
||||
Remotable
|
||||
remotedesktop
|
||||
remoteip
|
||||
Removelnk
|
||||
renamable
|
||||
@@ -1257,6 +1313,7 @@ RIGHTSCROLLBAR
|
||||
riid
|
||||
RKey
|
||||
RNumber
|
||||
rollups
|
||||
rop
|
||||
ROUNDSMALL
|
||||
rpcrt
|
||||
@@ -1289,7 +1346,7 @@ SCREENFONTS
|
||||
screensaver
|
||||
screenshots
|
||||
scrollviewer
|
||||
SDDL
|
||||
sddl
|
||||
SDKDDK
|
||||
sdns
|
||||
searchterm
|
||||
@@ -1468,6 +1525,9 @@ SVGIO
|
||||
svgz
|
||||
SVSI
|
||||
SWFO
|
||||
swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
symbolrequestprod
|
||||
SYMCACHE
|
||||
@@ -1484,6 +1544,8 @@ SYSKEY
|
||||
syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
SYSTEMTIME
|
||||
@@ -1570,6 +1632,9 @@ UHash
|
||||
UIA
|
||||
UIEx
|
||||
ULONGLONG
|
||||
Ultrawide
|
||||
UMax
|
||||
UMin
|
||||
ums
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
|
||||
21
.github/skills/winmd-api-search/LICENSE.txt
vendored
Normal file
21
.github/skills/winmd-api-search/LICENSE.txt
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
192
.github/skills/winmd-api-search/SKILL.md
vendored
Normal file
192
.github/skills/winmd-api-search/SKILL.md
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: winmd-api-search
|
||||
description: 'Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the right API for a task and retrieves full type details (methods, properties, events, enumeration values).'
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# WinMD API Search
|
||||
|
||||
This skill helps you find the right Windows API for any capability and get its full details. It searches a local cache of all WinMD metadata from:
|
||||
|
||||
- **Windows Platform SDK** — all `Windows.*` WinRT APIs (always available, no restore needed)
|
||||
- **WinAppSDK / WinUI** — bundled as a baseline in the cache generator (always available, no restore needed)
|
||||
- **NuGet packages** — any additional packages in restored projects that contain `.winmd` files
|
||||
- **Project-output WinMD** — class libraries (C++/WinRT, C#) that produce `.winmd` as build output
|
||||
|
||||
Even on a fresh clone with no restore or build, you still get full Platform SDK + WinAppSDK coverage.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- User wants to build a feature and you need to find which API provides that capability
|
||||
- User asks "how do I do X?" where X involves a platform feature (camera, files, notifications, sensors, AI, etc.)
|
||||
- You need the exact methods, properties, events, or enumeration values of a type before writing code
|
||||
- You're unsure which control, class, or interface to use for a UI or system task
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **.NET SDK 8.0 or later** — required to build the cache generator. Install from [dotnet.microsoft.com](https://dotnet.microsoft.com/download) if not available.
|
||||
|
||||
## Cache Setup (Required Before First Use)
|
||||
|
||||
All query and search commands read from a local JSON cache. **You must generate the cache before running any queries.**
|
||||
|
||||
```powershell
|
||||
# All projects in the repo (recommended for first run)
|
||||
.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1
|
||||
|
||||
# Single project
|
||||
.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1 -ProjectDir <project-folder>
|
||||
```
|
||||
|
||||
No project restore or build is needed for baseline coverage (Platform SDK + WinAppSDK). For additional NuGet packages, the project needs `dotnet restore` (which generates `project.assets.json`) or a `packages.config` file.
|
||||
|
||||
Cache is stored at `Generated Files\winmd-cache\`, deduplicated per-package+version.
|
||||
|
||||
### What gets indexed
|
||||
|
||||
| Source | When available |
|
||||
|--------|----------------|
|
||||
| Windows Platform SDK | Always (reads from local SDK install) |
|
||||
| WinAppSDK (latest) | Always (bundled as baseline in cache generator) |
|
||||
| WinAppSDK Runtime | When installed on the system (detected via `Get-AppxPackage`) |
|
||||
| Project NuGet packages | After `dotnet restore` or with `packages.config` |
|
||||
| Project-output `.winmd` | After project build (class libraries that produce WinMD) |
|
||||
|
||||
> **Note:** This cache directory should be in `.gitignore` — it's generated, not source.
|
||||
|
||||
## How to Use
|
||||
|
||||
Pick the path that matches the situation:
|
||||
|
||||
---
|
||||
|
||||
### Discover — "I don't know which API to use"
|
||||
|
||||
The user describes a capability in their own words. You need to find the right API.
|
||||
|
||||
**0. Ensure the cache exists**
|
||||
|
||||
If the cache hasn't been generated yet, run `Update-WinMdCache.ps1` first — see [Cache Setup](#cache-setup-required-before-first-use) above.
|
||||
|
||||
**1. Translate user language → search keywords**
|
||||
|
||||
Map the user's daily language to programming terms. Try multiple variations:
|
||||
|
||||
| User says | Search keywords to try (in order) |
|
||||
|-----------|-----------------------------------|
|
||||
| "take a picture" | `camera`, `capture`, `photo`, `MediaCapture` |
|
||||
| "load from disk" | `file open`, `picker`, `FileOpen`, `StorageFile` |
|
||||
| "describe what's in it" | `image description`, `Vision`, `Recognition` |
|
||||
| "show a popup" | `dialog`, `flyout`, `popup`, `ContentDialog` |
|
||||
| "drag and drop" | `drag`, `drop`, `DragDrop` |
|
||||
| "save settings" | `settings`, `ApplicationData`, `LocalSettings` |
|
||||
|
||||
Start with simple everyday words. If results are weak or irrelevant, try the more technical variation.
|
||||
|
||||
**2. Run searches**
|
||||
|
||||
```powershell
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action search -Query "<keyword>"
|
||||
```
|
||||
|
||||
This returns ranked namespaces with top matching types and the **JSON file path**.
|
||||
|
||||
If results have **low scores (below 60) or are irrelevant**, fall back to searching online documentation:
|
||||
|
||||
1. Use web search to find the right API on Microsoft Learn, for example:
|
||||
- `site:learn.microsoft.com/uwp/api <capability keywords>` for `Windows.*` APIs
|
||||
- `site:learn.microsoft.com/windows/windows-app-sdk/api/winrt <capability keywords>` for `Microsoft.*` WinAppSDK APIs
|
||||
2. Read the documentation pages to identify which type matches the user's requirement.
|
||||
3. Once you know the type name, come back and use `-Action members` or `-Action enums` to get the exact local signatures.
|
||||
|
||||
**3. Read the JSON to choose the right API**
|
||||
|
||||
Read the file at the path(s) from the top results. The JSON has all types in that namespace — full members, signatures, parameters, return types, enumeration values.
|
||||
|
||||
Read and decide which types and members fit the user's requirement.
|
||||
|
||||
**4. Look up official documentation for context**
|
||||
|
||||
The cache contains only signatures — no descriptions or usage guidance. For explanations, examples, and remarks, look up the type on Microsoft Learn:
|
||||
|
||||
| Namespace prefix | Documentation base URL |
|
||||
|-----------------|----------------------|
|
||||
| `Windows.*` | `https://learn.microsoft.com/uwp/api/{fully.qualified.typename}` |
|
||||
| `Microsoft.*` (WinAppSDK) | `https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/{fully.qualified.typename}` |
|
||||
|
||||
For example, `Microsoft.UI.Xaml.Controls.NavigationView` maps to:
|
||||
`https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.navigationview`
|
||||
|
||||
**5. Use the API knowledge to answer or write code**
|
||||
|
||||
---
|
||||
|
||||
### Lookup — "I know the API, show me the details"
|
||||
|
||||
You already know (or suspect) the type or namespace name. Go direct:
|
||||
|
||||
```powershell
|
||||
# Get all members of a known type
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.NavigationView"
|
||||
|
||||
# Get enum values
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
|
||||
|
||||
# List all types in a namespace
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
|
||||
|
||||
# Browse namespaces
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
|
||||
```
|
||||
|
||||
If you need full detail beyond what `-Action members` shows, use `-Action search` to get the JSON file path, then read the JSON file directly.
|
||||
|
||||
---
|
||||
|
||||
### Other Commands
|
||||
|
||||
```powershell
|
||||
# List cached projects
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action projects
|
||||
|
||||
# List packages for a project
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action packages
|
||||
|
||||
# Show stats
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action stats
|
||||
```
|
||||
|
||||
> If only one project is cached, `-Project` is auto-selected.
|
||||
> If multiple projects exist, add `-Project <name>` (use `-Action projects` to see available names).
|
||||
> In scan mode, manifest names include a short hash suffix to avoid collisions; you can pass the base project name without the suffix if it's unambiguous.
|
||||
|
||||
## Search Scoring
|
||||
|
||||
The search ranks type names and member names against your query:
|
||||
|
||||
| Score | Match type | Example |
|
||||
|-------|-----------|---------|
|
||||
| 100 | Exact name | `Button` → `Button` |
|
||||
| 80 | Starts with | `Navigation` → `NavigationView` |
|
||||
| 60 | Contains | `Dialog` → `ContentDialog` |
|
||||
| 50 | PascalCase initials | `ASB` → `AutoSuggestBox` |
|
||||
| 40 | Multi-keyword AND | `navigation item` → `NavigationViewItem` |
|
||||
| 20 | Fuzzy character match | `NavVw` → `NavigationView` |
|
||||
|
||||
Results are grouped by namespace. Higher-scored namespaces appear first.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| "Cache not found" | Run `Update-WinMdCache.ps1` |
|
||||
| "Multiple projects cached" | Add `-Project <name>` |
|
||||
| "Namespace not found" | Use `-Action namespaces` to list available ones |
|
||||
| "Type not found" | Use fully qualified name (e.g., `Microsoft.UI.Xaml.Controls.Button`) |
|
||||
| Stale after NuGet update | Re-run `Update-WinMdCache.ps1` |
|
||||
| Cache in git history | Add `Generated Files/` to `.gitignore` |
|
||||
|
||||
## References
|
||||
|
||||
- [Windows Platform SDK API reference](https://learn.microsoft.com/uwp/api/) — documentation for `Windows.*` namespaces
|
||||
- [Windows App SDK API reference](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/) — documentation for `Microsoft.*` WinAppSDK namespaces
|
||||
505
.github/skills/winmd-api-search/scripts/Invoke-WinMdQuery.ps1
vendored
Normal file
505
.github/skills/winmd-api-search/scripts/Invoke-WinMdQuery.ps1
vendored
Normal file
@@ -0,0 +1,505 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Query WinMD API metadata from cached JSON files.
|
||||
|
||||
.DESCRIPTION
|
||||
Reads pre-built JSON cache of WinMD types, members, and namespaces.
|
||||
The cache is organized per-package (deduplicated) with project manifests
|
||||
that map each project to its referenced packages.
|
||||
|
||||
Supports listing namespaces, types, members, searching, enum value lookup,
|
||||
and listing cached projects/packages.
|
||||
|
||||
.PARAMETER Action
|
||||
The query action to perform:
|
||||
- projects : List cached projects
|
||||
- packages : List packages for a project
|
||||
- stats : Show aggregate statistics for a project
|
||||
- namespaces : List all namespaces (optional -Filter prefix)
|
||||
- types : List types in a namespace (-Namespace required)
|
||||
- members : List members of a type (-TypeName required)
|
||||
- search : Search types and members by name (-Query required)
|
||||
- enums : List enum values (-TypeName required)
|
||||
|
||||
.PARAMETER Project
|
||||
Project name to query. Auto-selected if only one project is cached.
|
||||
Use -Action projects to list available projects.
|
||||
|
||||
.PARAMETER Namespace
|
||||
Namespace to query types from (used with -Action types).
|
||||
|
||||
.PARAMETER TypeName
|
||||
Full type name e.g. "Microsoft.UI.Xaml.Controls.Button" (used with -Action members, enums).
|
||||
|
||||
.PARAMETER Query
|
||||
Search query string (used with -Action search).
|
||||
|
||||
.PARAMETER Filter
|
||||
Optional prefix filter for namespaces (used with -Action namespaces).
|
||||
|
||||
.PARAMETER CacheDir
|
||||
Path to the winmd-cache directory. Defaults to "Generated Files\winmd-cache"
|
||||
relative to the workspace root.
|
||||
|
||||
.PARAMETER MaxResults
|
||||
Maximum number of results to return for search. Defaults to 30.
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-WinMdQuery.ps1 -Action projects
|
||||
.\Invoke-WinMdQuery.ps1 -Action packages -Project BlankWinUI
|
||||
.\Invoke-WinMdQuery.ps1 -Action stats -Project BlankWinUI
|
||||
.\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
|
||||
.\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
|
||||
.\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.Button"
|
||||
.\Invoke-WinMdQuery.ps1 -Action search -Query "NavigationView"
|
||||
.\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet('projects', 'packages', 'stats', 'namespaces', 'types', 'members', 'search', 'enums')]
|
||||
[string]$Action,
|
||||
|
||||
[string]$Project,
|
||||
[string]$Namespace,
|
||||
[string]$TypeName,
|
||||
[string]$Query,
|
||||
[string]$Filter,
|
||||
[string]$CacheDir,
|
||||
[int]$MaxResults = 30
|
||||
)
|
||||
|
||||
# ─── Resolve cache directory ─────────────────────────────────────────────────
|
||||
|
||||
if (-not $CacheDir) {
|
||||
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
|
||||
# so workspace root is 4 levels up from $PSScriptRoot.
|
||||
$scriptDir = $PSScriptRoot
|
||||
$root = (Resolve-Path (Join-Path $scriptDir '..\..\..\..')).Path
|
||||
$CacheDir = Join-Path $root 'Generated Files\winmd-cache'
|
||||
}
|
||||
|
||||
if (-not (Test-Path $CacheDir)) {
|
||||
Write-Error "Cache not found at: $CacheDir`nRun: .\Update-WinMdCache.ps1 (from .github\skills\winmd-api-search\scripts\)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ─── Project resolution helpers ──────────────────────────────────────────────
|
||||
|
||||
function Get-CachedProjects {
|
||||
$projectsDir = Join-Path $CacheDir 'projects'
|
||||
if (-not (Test-Path $projectsDir)) { return @() }
|
||||
Get-ChildItem $projectsDir -Filter '*.json' | ForEach-Object { $_.BaseName }
|
||||
}
|
||||
|
||||
function Resolve-ProjectManifest {
|
||||
param([string]$Name)
|
||||
|
||||
$projectsDir = Join-Path $CacheDir 'projects'
|
||||
if (-not (Test-Path $projectsDir)) {
|
||||
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Name) {
|
||||
$path = Join-Path $projectsDir "$Name.json"
|
||||
if (-not (Test-Path $path)) {
|
||||
# Scan mode appends a hash suffix -- try prefix match
|
||||
$matching = @(Get-ChildItem $projectsDir -Filter "${Name}_*.json" -ErrorAction SilentlyContinue)
|
||||
if ($matching.Count -eq 1) {
|
||||
return Get-Content $matching[0].FullName -Raw | ConvertFrom-Json
|
||||
}
|
||||
if ($matching.Count -gt 1) {
|
||||
$names = ($matching | ForEach-Object { $_.BaseName }) -join ', '
|
||||
Write-Error "Multiple projects match '$Name'. Specify the full name: $names"
|
||||
exit 1
|
||||
}
|
||||
$available = (Get-CachedProjects) -join ', '
|
||||
Write-Error "Project '$Name' not found. Available: $available"
|
||||
exit 1
|
||||
}
|
||||
return Get-Content $path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
# Auto-select if only one project
|
||||
$manifests = Get-ChildItem $projectsDir -Filter '*.json' -ErrorAction SilentlyContinue
|
||||
if ($manifests.Count -eq 0) {
|
||||
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
|
||||
exit 1
|
||||
}
|
||||
if ($manifests.Count -eq 1) {
|
||||
return Get-Content $manifests[0].FullName -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
$available = ($manifests | ForEach-Object { $_.BaseName }) -join ', '
|
||||
Write-Error "Multiple projects cached -- use -Project to specify. Available: $available"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Get-PackageCacheDirs {
|
||||
param($Manifest)
|
||||
$dirs = @()
|
||||
foreach ($pkg in $Manifest.packages) {
|
||||
$dir = Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version
|
||||
if (Test-Path $dir) {
|
||||
$dirs += $dir
|
||||
}
|
||||
}
|
||||
return $dirs
|
||||
}
|
||||
|
||||
# ─── Action: projects ────────────────────────────────────────────────────────
|
||||
|
||||
function Show-Projects {
|
||||
$projects = Get-CachedProjects
|
||||
if ($projects.Count -eq 0) {
|
||||
Write-Output "No projects cached."
|
||||
return
|
||||
}
|
||||
Write-Output "Cached projects ($($projects.Count)):"
|
||||
foreach ($p in $projects) {
|
||||
$manifest = Get-Content (Join-Path (Join-Path $CacheDir 'projects') "$p.json") -Raw | ConvertFrom-Json
|
||||
$pkgCount = $manifest.packages.Count
|
||||
Write-Output " $p ($pkgCount package(s))"
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Action: packages ────────────────────────────────────────────────────────
|
||||
|
||||
function Show-Packages {
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
Write-Output "Packages for project '$($manifest.projectName)' ($($manifest.packages.Count)):"
|
||||
foreach ($pkg in $manifest.packages) {
|
||||
$metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
|
||||
if (Test-Path $metaPath) {
|
||||
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
Write-Output " $($pkg.id)@$($pkg.version) -- $($meta.totalTypes) types, $($meta.totalMembers) members"
|
||||
} else {
|
||||
Write-Output " $($pkg.id)@$($pkg.version) -- (cache missing)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Action: stats ───────────────────────────────────────────────────────────
|
||||
|
||||
function Show-Stats {
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$totalTypes = 0
|
||||
$totalMembers = 0
|
||||
$totalNamespaces = 0
|
||||
$totalWinMd = 0
|
||||
|
||||
foreach ($pkg in $manifest.packages) {
|
||||
$metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
|
||||
if (Test-Path $metaPath) {
|
||||
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
$totalTypes += $meta.totalTypes
|
||||
$totalMembers += $meta.totalMembers
|
||||
$totalNamespaces += $meta.totalNamespaces
|
||||
$totalWinMd += $meta.winMdFiles.Count
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "WinMD Index Statistics -- $($manifest.projectName)"
|
||||
Write-Output "======================================"
|
||||
Write-Output " Packages: $($manifest.packages.Count)"
|
||||
Write-Output " Namespaces: $totalNamespaces (may overlap across packages)"
|
||||
Write-Output " Types: $totalTypes"
|
||||
Write-Output " Members: $totalMembers"
|
||||
Write-Output " WinMD files: $totalWinMd"
|
||||
}
|
||||
|
||||
# ─── Action: namespaces ──────────────────────────────────────────────────────
|
||||
|
||||
function Get-Namespaces {
|
||||
param([string]$Prefix)
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
$allNs = @()
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$nsFile = Join-Path $dir 'namespaces.json'
|
||||
if (Test-Path $nsFile) {
|
||||
$allNs += (Get-Content $nsFile -Raw | ConvertFrom-Json)
|
||||
}
|
||||
}
|
||||
|
||||
$allNs = $allNs | Sort-Object -Unique
|
||||
if ($Prefix) {
|
||||
$allNs = $allNs | Where-Object { $_ -like "$Prefix*" }
|
||||
}
|
||||
$allNs | ForEach-Object { Write-Output $_ }
|
||||
}
|
||||
|
||||
# ─── Action: types ───────────────────────────────────────────────────────────
|
||||
|
||||
function Get-TypesInNamespace {
|
||||
param([string]$Ns)
|
||||
if (-not $Ns) {
|
||||
Write-Error "-Namespace is required for 'types' action."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
$safeFile = $Ns.Replace('.', '_') + '.json'
|
||||
$found = $false
|
||||
$seen = @{}
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$filePath = Join-Path $dir "types\$safeFile"
|
||||
if (-not (Test-Path $filePath)) { continue }
|
||||
$found = $true
|
||||
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
||||
foreach ($t in $types) {
|
||||
if ($seen.ContainsKey($t.fullName)) { continue }
|
||||
$seen[$t.fullName] = $true
|
||||
Write-Output "$($t.kind) $($t.fullName)$(if ($t.baseType) { " : $($t.baseType)" } else { '' })"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Error "Namespace not found: $Ns"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Action: members ─────────────────────────────────────────────────────────
|
||||
|
||||
function Get-MembersOfType {
|
||||
param([string]$FullName)
|
||||
if (-not $FullName) {
|
||||
Write-Error "-TypeName is required for 'members' action."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$lastDot = $FullName.LastIndexOf('.')
|
||||
if ($lastDot -lt 0) {
|
||||
Write-Error "-TypeName must include a namespace (for example: 'MyNamespace.MyType'). Provided: $FullName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$ns = $FullName.Substring(0, $lastDot)
|
||||
$safeFile = $ns.Replace('.', '_') + '.json'
|
||||
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$filePath = Join-Path $dir "types\$safeFile"
|
||||
if (-not (Test-Path $filePath)) { continue }
|
||||
|
||||
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
||||
$type = $types | Where-Object { $_.fullName -eq $FullName }
|
||||
if (-not $type) { continue }
|
||||
|
||||
Write-Output "$($type.kind) $($type.fullName)"
|
||||
if ($type.baseType) { Write-Output " Extends: $($type.baseType)" }
|
||||
Write-Output ""
|
||||
foreach ($m in $type.members) {
|
||||
Write-Output " [$($m.kind)] $($m.signature)"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Write-Error "Type not found: $FullName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ─── Action: search ──────────────────────────────────────────────────────────
|
||||
# Ranks namespaces by best match score on type names and member names.
|
||||
# Outputs: ranked namespaces with top matching types and the JSON file path.
|
||||
# The agent can then read the JSON file to inspect all members intelligently.
|
||||
|
||||
function Search-WinMd {
|
||||
param([string]$SearchQuery, [int]$Max)
|
||||
if (-not $SearchQuery) {
|
||||
Write-Error "-Query is required for 'search' action."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
|
||||
# Collect: namespace -> { bestScore, matchingTypes[], filePath }
|
||||
$nsResults = @{}
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$nsFile = Join-Path $dir 'namespaces.json'
|
||||
if (-not (Test-Path $nsFile)) { continue }
|
||||
$nsList = Get-Content $nsFile -Raw | ConvertFrom-Json
|
||||
|
||||
foreach ($n in $nsList) {
|
||||
$safeFile = $n.Replace('.', '_') + '.json'
|
||||
$filePath = Join-Path $dir "types\$safeFile"
|
||||
if (-not (Test-Path $filePath)) { continue }
|
||||
|
||||
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
||||
foreach ($t in $types) {
|
||||
$typeScore = Get-MatchScore -Name $t.name -FullName $t.fullName -Query $SearchQuery
|
||||
|
||||
# Also search member names for matches
|
||||
$bestMemberScore = 0
|
||||
$matchingMember = $null
|
||||
if ($t.members) {
|
||||
foreach ($m in $t.members) {
|
||||
$memberName = $m.name
|
||||
$mScore = Get-MatchScore -Name $memberName -FullName "$($t.fullName).$memberName" -Query $SearchQuery
|
||||
if ($mScore -gt $bestMemberScore) {
|
||||
$bestMemberScore = $mScore
|
||||
$matchingMember = $m.signature
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$score = [Math]::Max($typeScore, $bestMemberScore)
|
||||
if ($score -le 0) { continue }
|
||||
|
||||
if (-not $nsResults.ContainsKey($n)) {
|
||||
$nsResults[$n] = @{ BestScore = 0; Types = @(); FilePaths = @() }
|
||||
}
|
||||
$entry = $nsResults[$n]
|
||||
if ($score -gt $entry.BestScore) { $entry.BestScore = $score }
|
||||
if ($entry.FilePaths -notcontains $filePath) {
|
||||
$entry.FilePaths += $filePath
|
||||
}
|
||||
|
||||
if ($typeScore -ge $bestMemberScore) {
|
||||
$entry.Types += @{ Text = "$($t.kind) $($t.fullName) [$typeScore]"; Score = $typeScore }
|
||||
} else {
|
||||
$entry.Types += @{ Text = "$($t.kind) $($t.fullName) -> $matchingMember [$bestMemberScore]"; Score = $bestMemberScore }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($nsResults.Count -eq 0) {
|
||||
Write-Output "No results found for: $SearchQuery"
|
||||
return
|
||||
}
|
||||
|
||||
$ranked = $nsResults.GetEnumerator() |
|
||||
Sort-Object { $_.Value.BestScore } -Descending |
|
||||
Select-Object -First $Max
|
||||
|
||||
foreach ($r in $ranked) {
|
||||
$ns = $r.Key
|
||||
$info = $r.Value
|
||||
Write-Output "[$($info.BestScore)] $ns"
|
||||
foreach ($fp in $info.FilePaths) {
|
||||
Write-Output " File: $fp"
|
||||
}
|
||||
# Show top 5 highest-scoring matching types in this namespace
|
||||
$info.Types | Sort-Object { $_.Score } -Descending |
|
||||
Select-Object -First 5 |
|
||||
ForEach-Object { Write-Output " $($_.Text)" }
|
||||
Write-Output ""
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Search scoring ──────────────────────────────────────────────────────────
|
||||
# Simple ranked scoring on type names. Higher = better.
|
||||
# 100 = exact name 80 = starts-with 60 = substring
|
||||
# 50 = PascalCase 40 = multi-keyword 20 = fuzzy subsequence
|
||||
|
||||
function Get-MatchScore {
|
||||
param([string]$Name, [string]$FullName, [string]$Query)
|
||||
|
||||
$q = $Query.Trim()
|
||||
if (-not $q) { return 0 }
|
||||
|
||||
if ($Name -eq $q) { return 100 }
|
||||
if ($Name -like "$q*") { return 80 }
|
||||
if ($Name -like "*$q*" -or $FullName -like "*$q*") { return 60 }
|
||||
|
||||
$initials = ($Name.ToCharArray() | Where-Object { [char]::IsUpper($_) }) -join ''
|
||||
if ($initials.Length -ge 2 -and $initials -like "*$q*") { return 50 }
|
||||
|
||||
$words = $q -split '\s+' | Where-Object { $_.Length -gt 0 }
|
||||
if ($words.Count -gt 1) {
|
||||
$allFound = $true
|
||||
foreach ($w in $words) {
|
||||
if ($Name -notlike "*$w*" -and $FullName -notlike "*$w*") {
|
||||
$allFound = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($allFound) { return 40 }
|
||||
}
|
||||
|
||||
if (Test-FuzzySubsequence -Text $Name -Pattern $q) { return 20 }
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function Test-FuzzySubsequence {
|
||||
param([string]$Text, [string]$Pattern)
|
||||
$ti = 0
|
||||
$tLower = $Text.ToLowerInvariant()
|
||||
$pLower = $Pattern.ToLowerInvariant()
|
||||
foreach ($ch in $pLower.ToCharArray()) {
|
||||
$idx = $tLower.IndexOf($ch, $ti)
|
||||
if ($idx -lt 0) { return $false }
|
||||
$ti = $idx + 1
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
# ─── Action: enums ───────────────────────────────────────────────────────────
|
||||
|
||||
function Get-EnumValues {
|
||||
param([string]$FullName)
|
||||
if (-not $FullName) {
|
||||
Write-Error "-TypeName is required for 'enums' action."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$lastDot = $FullName.LastIndexOf('.')
|
||||
if ($lastDot -lt 1) {
|
||||
Write-Error "-TypeName must be a fully-qualified type name including namespace, e.g. 'Namespace.TypeName'. Provided: $FullName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$ns = $FullName.Substring(0, $lastDot)
|
||||
$safeFile = $ns.Replace('.', '_') + '.json'
|
||||
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$filePath = Join-Path $dir "types\$safeFile"
|
||||
if (-not (Test-Path $filePath)) { continue }
|
||||
|
||||
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
||||
$type = $types | Where-Object { $_.fullName -eq $FullName }
|
||||
if (-not $type) { continue }
|
||||
|
||||
if ($type.kind -ne 'Enum') {
|
||||
Write-Error "$FullName is not an Enum (kind: $($type.kind))"
|
||||
exit 1
|
||||
}
|
||||
Write-Output "Enum $($type.fullName)"
|
||||
if ($type.enumValues) {
|
||||
$type.enumValues | ForEach-Object { Write-Output " $_" }
|
||||
} else {
|
||||
Write-Output " (no values)"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Write-Error "Type not found: $FullName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ─── Dispatch ─────────────────────────────────────────────────────────────────
|
||||
|
||||
switch ($Action) {
|
||||
'projects' { Show-Projects }
|
||||
'packages' { Show-Packages }
|
||||
'stats' { Show-Stats }
|
||||
'namespaces' { Get-Namespaces -Prefix $Filter }
|
||||
'types' { Get-TypesInNamespace -Ns $Namespace }
|
||||
'members' { Get-MembersOfType -FullName $TypeName }
|
||||
'search' { Search-WinMd -SearchQuery $Query -Max $MaxResults }
|
||||
'enums' { Get-EnumValues -FullName $TypeName }
|
||||
}
|
||||
208
.github/skills/winmd-api-search/scripts/Update-WinMdCache.ps1
vendored
Normal file
208
.github/skills/winmd-api-search/scripts/Update-WinMdCache.ps1
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generate or refresh the WinMD cache for the Agent Skill.
|
||||
|
||||
.DESCRIPTION
|
||||
Builds and runs the standalone cache generator to export cached JSON files
|
||||
from all WinMD metadata found in project NuGet packages and Windows SDK.
|
||||
|
||||
The cache is per-package+version: if two projects reference the same
|
||||
package at the same version, the WinMD data is parsed once and shared.
|
||||
|
||||
Supports single project or recursive scan of an entire repo.
|
||||
|
||||
.PARAMETER ProjectDir
|
||||
Path to a project directory (contains .csproj/.vcxproj), or a project file itself.
|
||||
Defaults to scanning the workspace root.
|
||||
|
||||
.PARAMETER Scan
|
||||
Recursively discover all .csproj/.vcxproj files under ProjectDir.
|
||||
|
||||
.PARAMETER OutputDir
|
||||
Path to the cache output directory. Defaults to "Generated Files\winmd-cache".
|
||||
|
||||
.EXAMPLE
|
||||
.\Update-WinMdCache.ps1
|
||||
.\Update-WinMdCache.ps1 -ProjectDir BlankWinUI
|
||||
.\Update-WinMdCache.ps1 -Scan -ProjectDir .
|
||||
.\Update-WinMdCache.ps1 -ProjectDir "src\MyApp\MyApp.csproj"
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ProjectDir,
|
||||
[switch]$Scan,
|
||||
[string]$OutputDir = 'Generated Files\winmd-cache'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
|
||||
# so workspace root is 4 levels up from $PSScriptRoot.
|
||||
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path
|
||||
$generatorProj = Join-Path (Join-Path $PSScriptRoot 'cache-generator') 'CacheGenerator.csproj'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WinAppSDK version detection -- look only at the repo root folder (no recursion)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Get-WinAppSdkVersionFromDirectoryPackagesProps {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extract Microsoft.WindowsAppSDK version from a Directory.Packages.props
|
||||
(Central Package Management) at the repo root.
|
||||
#>
|
||||
param([string]$RepoRoot)
|
||||
$propsFile = Join-Path $RepoRoot 'Directory.Packages.props'
|
||||
if (-not (Test-Path $propsFile)) { return $null }
|
||||
try {
|
||||
[xml]$xml = Get-Content $propsFile -Raw
|
||||
$node = $xml.SelectNodes('//PackageVersion') |
|
||||
Where-Object { $_.Include -eq 'Microsoft.WindowsAppSDK' } |
|
||||
Select-Object -First 1
|
||||
if ($node) { return $node.Version }
|
||||
} catch {
|
||||
Write-Verbose "Could not parse $propsFile : $_"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-WinAppSdkVersionFromPackagesConfig {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extract Microsoft.WindowsAppSDK version from a packages.config at the repo root.
|
||||
#>
|
||||
param([string]$RepoRoot)
|
||||
$configFile = Join-Path $RepoRoot 'packages.config'
|
||||
if (-not (Test-Path $configFile)) { return $null }
|
||||
try {
|
||||
[xml]$xml = Get-Content $configFile -Raw
|
||||
$node = $xml.SelectNodes('//package') |
|
||||
Where-Object { $_.id -eq 'Microsoft.WindowsAppSDK' } |
|
||||
Select-Object -First 1
|
||||
if ($node) { return $node.version }
|
||||
} catch {
|
||||
Write-Verbose "Could not parse $configFile : $_"
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Try Directory.Packages.props first (CPM), then packages.config
|
||||
$winAppSdkVersion = Get-WinAppSdkVersionFromDirectoryPackagesProps -RepoRoot $root
|
||||
if (-not $winAppSdkVersion) {
|
||||
$winAppSdkVersion = Get-WinAppSdkVersionFromPackagesConfig -RepoRoot $root
|
||||
}
|
||||
if ($winAppSdkVersion) {
|
||||
Write-Host "Detected WinAppSDK version from repo: $winAppSdkVersion" -ForegroundColor Cyan
|
||||
} else {
|
||||
Write-Host "No WinAppSDK version found at repo root; will use latest (Version=*)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Default: if no ProjectDir, scan the workspace root
|
||||
if (-not $ProjectDir) {
|
||||
$ProjectDir = $root
|
||||
$Scan = $true
|
||||
}
|
||||
|
||||
Push-Location $root
|
||||
|
||||
try {
|
||||
# Detect installed .NET SDK -- require >= 8.0, prefer stable over preview
|
||||
$dotnetSdks = dotnet --list-sdks 2>$null
|
||||
$bestMajor = $dotnetSdks |
|
||||
Where-Object { $_ -notmatch 'preview|rc|alpha|beta' } |
|
||||
ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } |
|
||||
Where-Object { $_ -ge 8 } |
|
||||
Sort-Object -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
# Fall back to preview SDKs if no stable SDK found
|
||||
if (-not $bestMajor) {
|
||||
$bestMajor = $dotnetSdks |
|
||||
ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } |
|
||||
Where-Object { $_ -ge 8 } |
|
||||
Sort-Object -Descending |
|
||||
Select-Object -First 1
|
||||
}
|
||||
|
||||
if (-not $bestMajor) {
|
||||
Write-Error "No .NET SDK >= 8.0 found. Install from https://dotnet.microsoft.com/download"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$targetFramework = "net$bestMajor.0"
|
||||
Write-Host "Using .NET SDK: $targetFramework" -ForegroundColor Cyan
|
||||
|
||||
# Build MSBuild properties -- pass detected WinAppSDK version when available
|
||||
$sdkVersionProp = ''
|
||||
if ($winAppSdkVersion) {
|
||||
$sdkVersionProp = "-p:WinAppSdkVersion=$winAppSdkVersion"
|
||||
}
|
||||
|
||||
Write-Host "Building cache generator..." -ForegroundColor Cyan
|
||||
$restoreArgs = @($generatorProj, "-p:TargetFramework=$targetFramework", '--nologo', '-v', 'q')
|
||||
if ($sdkVersionProp) { $restoreArgs += $sdkVersionProp }
|
||||
dotnet restore @restoreArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Restore failed"
|
||||
exit 1
|
||||
}
|
||||
$buildArgs = @($generatorProj, '-c', 'Release', '--nologo', '-v', 'q', "-p:TargetFramework=$targetFramework", '--no-restore')
|
||||
if ($sdkVersionProp) { $buildArgs += $sdkVersionProp }
|
||||
dotnet build @buildArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Build failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Run the built executable directly (avoids dotnet run target framework mismatch issues)
|
||||
$generatorDir = Join-Path $PSScriptRoot 'cache-generator'
|
||||
$exePath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.exe"
|
||||
if (-not (Test-Path $exePath)) {
|
||||
# Fallback: try dll with dotnet
|
||||
$dllPath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.dll"
|
||||
if (Test-Path $dllPath) {
|
||||
$exePath = $null
|
||||
} else {
|
||||
Write-Error "Built executable not found at: $exePath"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$runArgs = @()
|
||||
if ($Scan) {
|
||||
$runArgs += '--scan'
|
||||
}
|
||||
|
||||
# Detect installed WinAppSDK runtime via Get-AppxPackage (the WindowsApps
|
||||
# folder is ACL-restricted so C# cannot enumerate it directly).
|
||||
# WinMD files are architecture-independent metadata, so pick whichever arch
|
||||
# matches the current OS to ensure the package is present.
|
||||
$osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
|
||||
$runtimePkg = Get-AppxPackage -Name 'Microsoft.WindowsAppRuntime.*' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -notmatch 'CBS' -and $_.Architecture -eq $osArch } |
|
||||
Sort-Object -Property Version -Descending |
|
||||
Select-Object -First 1
|
||||
if ($runtimePkg -and $runtimePkg.InstallLocation -and (Test-Path $runtimePkg.InstallLocation)) {
|
||||
Write-Host "Detected WinAppSDK runtime: $($runtimePkg.Name) v$($runtimePkg.Version)" -ForegroundColor Cyan
|
||||
$runArgs += '--winappsdk-runtime'
|
||||
$runArgs += $runtimePkg.InstallLocation
|
||||
}
|
||||
|
||||
$runArgs += $ProjectDir
|
||||
$runArgs += $OutputDir
|
||||
|
||||
Write-Host "Exporting WinMD cache..." -ForegroundColor Cyan
|
||||
if ($exePath) {
|
||||
& $exePath @runArgs
|
||||
} else {
|
||||
dotnet $dllPath @runArgs
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Cache export failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Cache updated at: $OutputDir" -ForegroundColor Green
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
29
.github/skills/winmd-api-search/scripts/cache-generator/CacheGenerator.csproj
vendored
Normal file
29
.github/skills/winmd-api-search/scripts/cache-generator/CacheGenerator.csproj
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<!-- Default fallback; Update-WinMdCache.ps1 overrides via -p:TargetFramework=net{X}.0 -->
|
||||
<TargetFramework Condition="'$(TargetFramework)' == ''">net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<!-- System.Reflection.Metadata is inbox in net9.0+, only needed for net8.0 -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="System.Reflection.Metadata" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Baseline WinAppSDK packages: downloaded during restore so the cache generator
|
||||
can always index WinAppSDK APIs, even if the target project hasn't been restored.
|
||||
ExcludeAssets="all" means they're downloaded but don't affect this tool's build.
|
||||
|
||||
When the repo has a known version (passed via -p:WinAppSdkVersion=X.Y.Z from
|
||||
Update-WinMdCache.ps1), prefer that version to avoid unnecessary NuGet downloads.
|
||||
Falls back to Version="*" (latest) on fresh clones with no restore.
|
||||
-->
|
||||
<ItemGroup Condition="'$(WinAppSdkVersion)' != ''">
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="$(WinAppSdkVersion)" ExcludeAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(WinAppSdkVersion)' == ''">
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="*" ExcludeAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
3
.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.props
vendored
Normal file
3
.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.props
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<Project>
|
||||
<!-- Isolate this standalone tool from the repo-level build configuration -->
|
||||
</Project>
|
||||
3
.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.targets
vendored
Normal file
3
.github/skills/winmd-api-search/scripts/cache-generator/Directory.Build.targets
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<Project>
|
||||
<!-- Isolate this standalone tool from the repo-level build targets -->
|
||||
</Project>
|
||||
3
.github/skills/winmd-api-search/scripts/cache-generator/Directory.Packages.props
vendored
Normal file
3
.github/skills/winmd-api-search/scripts/cache-generator/Directory.Packages.props
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<Project>
|
||||
<!-- Isolate this standalone tool from the repo-level Central Package Management -->
|
||||
</Project>
|
||||
1222
.github/skills/winmd-api-search/scripts/cache-generator/Program.cs
vendored
Normal file
1222
.github/skills/winmd-api-search/scripts/cache-generator/Program.cs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
165
.github/skills/wpf-to-winui3-migration/SKILL.md
vendored
Normal file
165
.github/skills/wpf-to-winui3-migration/SKILL.md
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: wpf-to-winui3-migration
|
||||
description: Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# WPF to WinUI 3 Migration Skill
|
||||
|
||||
Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.UI.Xaml.*` / Windows App SDK). Based on patterns validated in the ImageResizer module migration.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Migrate a PowerToys module from WPF to WinUI 3
|
||||
- Convert WPF XAML files to WinUI 3 XAML
|
||||
- Replace `System.Windows` namespaces with `Microsoft.UI.Xaml`
|
||||
- Migrate `Dispatcher` usage to `DispatcherQueue`
|
||||
- Migrate custom `Observable`/`RelayCommand` to CommunityToolkit.Mvvm source generators
|
||||
- Replace WPF-UI (Lepo) controls with native WinUI 3 controls
|
||||
- Convert imaging code from `System.Windows.Media.Imaging` to `Windows.Graphics.Imaging`
|
||||
- Handle WPF `Window` vs WinUI `Window` differences (sizing, positioning, SizeToContent)
|
||||
- Migrate resource files from `.resx` to `.resw` with `ResourceLoader`
|
||||
- Fix installer/build pipeline issues after WinUI 3 migration
|
||||
- Update project files, NuGet packages, and signing config
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Visual Studio 2022 17.4+
|
||||
- Windows App SDK NuGet package (`Microsoft.WindowsAppSDK`)
|
||||
- .NET 8+ with `net8.0-windows10.0.19041.0` TFM
|
||||
- Windows 10 1803+ (April 2018 Update or newer)
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Recommended Order
|
||||
|
||||
1. **Project file** — Update TFM, NuGet packages, set `<UseWinUI>true</UseWinUI>`
|
||||
2. **Data models and business logic** — No UI dependencies, migrate first
|
||||
3. **MVVM framework** — Replace custom Observable/RelayCommand with CommunityToolkit.Mvvm
|
||||
4. **Resource strings** — Migrate `.resx` → `.resw`, introduce `ResourceLoaderInstance`
|
||||
5. **Services and utilities** — Replace `System.Windows` types, async-ify imaging code
|
||||
6. **ViewModels** — Update Dispatcher usage, binding patterns
|
||||
7. **Views/Pages** — Starting from leaf pages with fewest dependencies
|
||||
8. **Main page / shell** — Last, since it depends on everything
|
||||
9. **App.xaml / startup code** — Merge carefully (do NOT overwrite WinUI 3 boilerplate)
|
||||
10. **Installer & build pipeline** — Update WiX, signing, build events
|
||||
11. **Tests** — Adapt for WinUI 3 runtime, async patterns
|
||||
|
||||
### Key Principles
|
||||
|
||||
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different application lifecycle boilerplate. Merge your resources and initialization code into the generated WinUI 3 App class.
|
||||
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. This causes phantom build artifacts.
|
||||
- **Use `Lazy<T>` for resource-dependent statics** — `ResourceLoader` is not available at class-load time in all contexts.
|
||||
|
||||
## Quick Reference Tables
|
||||
|
||||
### Namespace Mapping
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` |
|
||||
|
||||
### Critical API Replacements
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`) |
|
||||
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` | Property vs method |
|
||||
| `Application.Current.Dispatcher` | Store `DispatcherQueue` in static field | See [Threading](./references/threading-and-windowing.md) |
|
||||
| `MessageBox.Show()` | `ContentDialog` | Must set `XamlRoot` |
|
||||
| `DynamicResource` | `ThemeResource` | Theme-reactive only |
|
||||
| `clr-namespace:` | `using:` | XAML namespace prefix |
|
||||
| `{x:Static props:Resources.Key}` | `x:Uid` or `ResourceLoader.GetString()` | .resx → .resw |
|
||||
| `DataType="{x:Type m:Foo}"` | Remove or use code-behind | `x:Type` not supported |
|
||||
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
|
||||
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
|
||||
| `SizeToContent="Height"` | Custom `SizeToContent()` via `AppWindow.Resize()` | See [Windowing](./references/threading-and-windowing.md) |
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Mouse → Pointer events |
|
||||
| `Pack URI (pack://...)` | `ms-appx:///` | Resource URI scheme |
|
||||
| `Observable` (custom base) | `ObservableObject` + `[ObservableProperty]` | CommunityToolkit.Mvvm |
|
||||
| `RelayCommand` (custom) | `[RelayCommand]` source generator | CommunityToolkit.Mvvm |
|
||||
| `JpegBitmapEncoder` | `BitmapEncoder.CreateAsync(JpegEncoderId, stream)` | Async, unified API |
|
||||
| `encoder.QualityLevel = 85` | `BitmapPropertySet { "ImageQuality", 0.85f }` | int 1-100 → float 0-1 |
|
||||
|
||||
### NuGet Package Migration
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` |
|
||||
| `WPF-UI` (Lepo) | Remove — use native WinUI 3 controls |
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` |
|
||||
| (none) | `Microsoft.WindowsAppSDK` |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` |
|
||||
| (none) | `WinUIEx` (optional, for window helpers) |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` |
|
||||
|
||||
### XAML Syntax Changes
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` |
|
||||
| `{DynamicResource Key}` | `{ThemeResource Key}` |
|
||||
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind |
|
||||
| `{x:Type local:MyType}` | Not supported |
|
||||
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` |
|
||||
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` |
|
||||
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` |
|
||||
| `<ui:FluentWindow>` / `<ui:Button>` (WPF-UI) | Native `<Window>` / `<Button>` |
|
||||
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` |
|
||||
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` |
|
||||
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / handle via KeyDown |
|
||||
| `<AccessText>` | Not available — use `AccessKey` property |
|
||||
| `<behaviors:Interaction.Triggers>` | Migrate to code-behind or WinUI behaviors |
|
||||
|
||||
## Detailed Reference Docs
|
||||
|
||||
Read only the section relevant to your current task:
|
||||
|
||||
- [Namespace and API Mapping](./references/namespace-api-mapping.md) — Full type mapping, NuGet changes, project file, CsWinRT interop
|
||||
- [XAML Migration Guide](./references/xaml-migration.md) — XAML syntax, WPF-UI removal, markup extensions, styles, resources, data binding
|
||||
- [Threading and Window Management](./references/threading-and-windowing.md) — Dispatcher, DispatcherQueue, SizeToContent, AppWindow, HWND interop, custom entry point
|
||||
- [Imaging API Migration](./references/imaging-migration.md) — BitmapEncoder/Decoder, SoftwareBitmap, CodecHelper, async patterns, int→uint
|
||||
- [PowerToys-Specific Patterns](./references/powertoys-patterns.md) — MVVM migration, ResourceLoader, Lazy init, installer, signing, test adaptation, build pipeline
|
||||
|
||||
## Common Pitfalls (from ImageResizer migration)
|
||||
|
||||
| Pitfall | Solution |
|
||||
|---------|----------|
|
||||
| `ContentDialog` throws "does not have a XamlRoot" | Set `dialog.XamlRoot = this.Content.XamlRoot` before `ShowAsync()` |
|
||||
| `FilePicker` throws error in desktop app | Call `WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)` |
|
||||
| `Window.Dispatcher` returns null | Use `Window.DispatcherQueue` instead |
|
||||
| Resources on `Window` element not found | Move resources to root layout container (`Grid.Resources`) |
|
||||
| `VisualStateManager` on `Window` fails | Use `UserControl` or `Page` inside the Window |
|
||||
| Satellite assembly installer errors (`WIX0103`) | Remove `.resources.dll` refs from `Resources.wxs`; WinUI 3 uses `.pri` |
|
||||
| Phantom `.exe`/`.deps.json` in root output dir | Avoid Exe→WinExe `ProjectReference`; use Library project |
|
||||
| `ResourceLoader` crash at static init | Wrap in `Lazy<T>` or null-coalescing property — see [Lazy Init](./references/powertoys-patterns.md#lazy-initialization-for-resource-dependent-statics) |
|
||||
| `SizeToContent` not available | Implement manual content measurement + `AppWindow.Resize()` with DPI scaling |
|
||||
| `x:Bind` default mode is `OneTime` | Explicitly set `Mode=OneWay` or `Mode=TwoWay` |
|
||||
| `DynamicResource` / `x:Static` not compiling | Replace with `ThemeResource` / `ResourceLoader` or `x:Uid` |
|
||||
| `IValueConverter.Convert` signature mismatch | Last param: `CultureInfo` → `string` (language tag) |
|
||||
| Test project can't resolve WPF types | Add `<UseWPF>true</UseWPF>` temporarily; remove after imaging migration |
|
||||
| Pixel dimension type mismatch (`int` vs `uint`) | WinRT uses `uint` for pixel sizes — add `u` suffix in test assertions |
|
||||
| `$(SolutionDir)` empty in standalone project build | Use `$(MSBuildThisFileDirectory)` with relative paths instead |
|
||||
| JPEG quality value wrong after migration | WPF: int 1-100; WinRT: float 0.0-1.0 |
|
||||
| MSIX packaging fails in PreBuildEvent | Move to PostBuildEvent; artifacts not ready at PreBuild time |
|
||||
| RC file icon path with forward slashes | Use double-backslash escaping: `..\\ui\\Assets\\icon.ico` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Build fails after namespace rename | Check for lingering `System.Windows` usings; some types have no direct equivalent |
|
||||
| Missing `PresentationCore.dll` at runtime | Ensure ALL imaging code uses `Windows.Graphics.Imaging`, not `System.Windows.Media.Imaging` |
|
||||
| `DataContext` not working on Window | WinUI 3 `Window` is not a `DependencyObject`; use a root `Page` or `UserControl` |
|
||||
| XAML designer not available | WinUI 3 does not support XAML Designer; use Hot Reload instead |
|
||||
| NuGet restore failures | Run `build-essentials.cmd` after adding `Microsoft.WindowsAppSDK` package |
|
||||
| `Parallel.ForEach` compilation error | Migrate to `Parallel.ForEachAsync` for async imaging operations |
|
||||
| Signing check fails on leaked artifacts | Run `generateAllFileComponents.ps1`; verify only `WinUI3Apps\\` paths in signing config |
|
||||
287
.github/skills/wpf-to-winui3-migration/references/imaging-migration.md
vendored
Normal file
287
.github/skills/wpf-to-winui3-migration/references/imaging-migration.md
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
# Imaging API Migration
|
||||
|
||||
Migrating from WPF (`System.Windows.Media.Imaging` / `PresentationCore.dll`) to WinRT (`Windows.Graphics.Imaging`). Based on the ImageResizer migration.
|
||||
|
||||
## Why This Migration Is Required
|
||||
|
||||
WinUI 3 apps deployed as self-contained do NOT include `PresentationCore.dll`. Any code using `System.Windows.Media.Imaging` will throw `FileNotFoundException` at runtime. ALL imaging code must use WinRT APIs.
|
||||
|
||||
| Purpose | Namespace |
|
||||
|---------|-----------|
|
||||
| UI display (`Image.Source`) | `Microsoft.UI.Xaml.Media.Imaging` |
|
||||
| Image processing (encode/decode/transform) | `Windows.Graphics.Imaging` |
|
||||
|
||||
## Architecture Change: Pipeline vs Declarative
|
||||
|
||||
The fundamental architecture differs:
|
||||
|
||||
**WPF**: In-memory pipeline of bitmap objects. Decode → transform → encode synchronously.
|
||||
```csharp
|
||||
var decoder = BitmapDecoder.Create(stream, ...);
|
||||
var transform = new TransformedBitmap(decoder.Frames[0], new ScaleTransform(...));
|
||||
var encoder = new JpegBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(transform, ...));
|
||||
encoder.Save(outputStream);
|
||||
```
|
||||
|
||||
**WinRT**: Declarative transform model. Configure transforms on the encoder, which handles pixel manipulation internally. All async.
|
||||
```csharp
|
||||
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
|
||||
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
|
||||
encoder.BitmapTransform.ScaledWidth = newWidth;
|
||||
encoder.BitmapTransform.ScaledHeight = newHeight;
|
||||
encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
|
||||
await encoder.FlushAsync();
|
||||
```
|
||||
|
||||
## Core Type Mapping
|
||||
|
||||
### Decoders
|
||||
|
||||
| WPF | WinRT | Notes |
|
||||
|-----|-------|-------|
|
||||
| `BitmapDecoder.Create(stream, options, cache)` | `BitmapDecoder.CreateAsync(stream)` | Async, auto-detects format |
|
||||
| `JpegBitmapDecoder` / `PngBitmapDecoder` / etc. | `BitmapDecoder.CreateAsync(stream)` | Single unified decoder |
|
||||
| `decoder.Frames[0]` | `await decoder.GetFrameAsync(0)` | Async frame access |
|
||||
| `decoder.Frames.Count` | `decoder.FrameCount` (uint) | `int` → `uint` |
|
||||
| `decoder.CodecInfo.ContainerFormat` | `decoder.DecoderInformation.CodecId` | Different property path |
|
||||
| `decoder.Frames[0].PixelWidth` (int) | `decoder.PixelWidth` (uint) | `int` → `uint` |
|
||||
| `WmpBitmapDecoder` | Not available | WMP/HDP not supported |
|
||||
|
||||
### Encoders
|
||||
|
||||
| WPF | WinRT | Notes |
|
||||
|-----|-------|-------|
|
||||
| `new JpegBitmapEncoder()` | `BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream)` | Async factory |
|
||||
| `new PngBitmapEncoder()` | `BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream)` | No interlace control |
|
||||
| `encoder.Frames.Add(frame)` | `encoder.SetSoftwareBitmap(bitmap)` | Different API |
|
||||
| `encoder.Save(stream)` | `await encoder.FlushAsync()` | Async |
|
||||
|
||||
### Encoder Properties (Strongly-Typed → BitmapPropertySet)
|
||||
|
||||
WPF had type-specific encoder subclasses. WinRT uses a generic property set:
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
case JpegBitmapEncoder jpeg: jpeg.QualityLevel = 85; // int 1-100
|
||||
case PngBitmapEncoder png: png.Interlace = PngInterlaceOption.On;
|
||||
case TiffBitmapEncoder tiff: tiff.Compression = TiffCompressOption.Lzw;
|
||||
|
||||
// WinRT — JPEG quality (float 0.0-1.0)
|
||||
await encoder.BitmapProperties.SetPropertiesAsync(new BitmapPropertySet
|
||||
{
|
||||
{ "ImageQuality", new BitmapTypedValue(0.85f, PropertyType.Single) }
|
||||
});
|
||||
|
||||
// WinRT — TIFF compression (via BitmapPropertySet at creation time)
|
||||
var props = new BitmapPropertySet
|
||||
{
|
||||
{ "TiffCompressionMethod", new BitmapTypedValue((byte)2, PropertyType.UInt8) }
|
||||
};
|
||||
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.TiffEncoderId, stream, props);
|
||||
```
|
||||
|
||||
**JPEG quality scale change**: WPF int `1-100` → WinRT float `0.0-1.0`. Divide by 100.
|
||||
|
||||
### Bitmap Types
|
||||
|
||||
| WPF | WinRT | Notes |
|
||||
|-----|-------|-------|
|
||||
| `BitmapSource` | `SoftwareBitmap` | Central pixel-data type |
|
||||
| `BitmapImage` | `BitmapImage` (in `Microsoft.UI.Xaml.Media.Imaging`) | UI display only |
|
||||
| `FormatConvertedBitmap` | `SoftwareBitmap.Convert()` | |
|
||||
| `TransformedBitmap` + `ScaleTransform` | `BitmapTransform` via encoder | Declarative |
|
||||
| `CroppedBitmap` | `BitmapTransform.Bounds` | |
|
||||
|
||||
### Metadata
|
||||
|
||||
| WPF | WinRT | Notes |
|
||||
|-----|-------|-------|
|
||||
| `BitmapMetadata` | `BitmapProperties` | Different API surface |
|
||||
| `BitmapMetadata.Clone()` | No equivalent | Cannot selectively clone |
|
||||
| Selective metadata removal | Not supported | All-or-nothing only |
|
||||
|
||||
**Two encoder creation strategies for metadata:**
|
||||
- `CreateForTranscodingAsync()` — preserves ALL metadata from source
|
||||
- `CreateAsync()` — creates fresh encoder with NO metadata
|
||||
|
||||
This eliminated ~258 lines of manual metadata manipulation code (`BitmapMetadataExtension.cs`) in ImageResizer.
|
||||
|
||||
### Interpolation Modes
|
||||
|
||||
| WPF `BitmapScalingMode` | WinRT `BitmapInterpolationMode` |
|
||||
|------------------------|-------------------------------|
|
||||
| `HighQuality` / `Fant` | `Fant` |
|
||||
| `Linear` | `Linear` |
|
||||
| `NearestNeighbor` | `NearestNeighbor` |
|
||||
| `Unspecified` / `LowQuality` | `Linear` |
|
||||
|
||||
## Stream Interop
|
||||
|
||||
WinRT imaging requires `IRandomAccessStream` instead of `System.IO.Stream`:
|
||||
|
||||
```csharp
|
||||
using var stream = File.OpenRead(path);
|
||||
var winrtStream = stream.AsRandomAccessStream(); // Extension method
|
||||
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
|
||||
```
|
||||
|
||||
**Critical**: For transcode, seek the input stream back to 0 before creating the encoder:
|
||||
```csharp
|
||||
winrtStream.Seek(0);
|
||||
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
|
||||
```
|
||||
|
||||
## CodecHelper Pattern (from ImageResizer)
|
||||
|
||||
WPF stored container format GUIDs in `settings.json`. WinRT uses different codec IDs. Create a `CodecHelper` to bridge them:
|
||||
|
||||
```csharp
|
||||
internal static class CodecHelper
|
||||
{
|
||||
// Maps WPF container format GUIDs (stored in settings JSON) to WinRT encoder IDs
|
||||
private static readonly Dictionary<Guid, Guid> LegacyGuidToEncoderId = new()
|
||||
{
|
||||
[new Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057")] = BitmapEncoder.JpegEncoderId,
|
||||
[new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf")] = BitmapEncoder.PngEncoderId,
|
||||
[new Guid("0af1d87e-fcfe-4188-bdeb-a7906471cbe3")] = BitmapEncoder.BmpEncoderId,
|
||||
[new Guid("163bcc30-e2e9-4f0b-961d-a3e9fdb788a3")] = BitmapEncoder.TiffEncoderId,
|
||||
[new Guid("1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5")] = BitmapEncoder.GifEncoderId,
|
||||
};
|
||||
|
||||
// Maps decoder IDs to corresponding encoder IDs
|
||||
private static readonly Dictionary<Guid, Guid> DecoderIdToEncoderId = new()
|
||||
{
|
||||
[BitmapDecoder.JpegDecoderId] = BitmapEncoder.JpegEncoderId,
|
||||
[BitmapDecoder.PngDecoderId] = BitmapEncoder.PngEncoderId,
|
||||
// ...
|
||||
};
|
||||
|
||||
public static Guid GetEncoderIdFromLegacyGuid(Guid legacyGuid)
|
||||
=> LegacyGuidToEncoderId.GetValueOrDefault(legacyGuid, Guid.Empty);
|
||||
|
||||
public static Guid GetEncoderIdForDecoder(BitmapDecoder decoder)
|
||||
=> DecoderIdToEncoderId.GetValueOrDefault(decoder.DecoderInformation.CodecId, Guid.Empty);
|
||||
}
|
||||
```
|
||||
|
||||
This preserves backward compatibility with existing `settings.json` files that contain WPF-era GUIDs.
|
||||
|
||||
## ImagingEnums Pattern (from ImageResizer)
|
||||
|
||||
WPF-specific enums (`PngInterlaceOption`, `TiffCompressOption`) from `System.Windows.Media.Imaging` are used in settings JSON. Create custom enums with identical integer values for backward-compatible deserialization:
|
||||
|
||||
```csharp
|
||||
// Replace System.Windows.Media.Imaging.PngInterlaceOption
|
||||
public enum PngInterlaceOption { Default = 0, On = 1, Off = 2 }
|
||||
|
||||
// Replace System.Windows.Media.Imaging.TiffCompressOption
|
||||
public enum TiffCompressOption { Default = 0, None = 1, Ccitt3 = 2, Ccitt4 = 3, Lzw = 4, Rle = 5, Zip = 6 }
|
||||
```
|
||||
|
||||
## Async Migration Patterns
|
||||
|
||||
### Method Signatures
|
||||
|
||||
All imaging operations become async:
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `void Execute(file, settings)` | `async Task ExecuteAsync(file, settings)` |
|
||||
| `IEnumerable<Error> Process()` | `async Task<IEnumerable<Error>> ProcessAsync()` |
|
||||
|
||||
### Parallel Processing
|
||||
|
||||
```csharp
|
||||
// WPF (synchronous)
|
||||
Parallel.ForEach(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
|
||||
(file, state, i) => { Execute(file, settings); });
|
||||
|
||||
// WinRT (async)
|
||||
await Parallel.ForEachAsync(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
|
||||
async (file, ct) => { await ExecuteAsync(file, settings); });
|
||||
```
|
||||
|
||||
### CLI Async Bridge
|
||||
|
||||
CLI entry points must bridge async to sync:
|
||||
```csharp
|
||||
return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
|
||||
```
|
||||
|
||||
### Task.Factory.StartNew → Task.Run
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
_ = Task.Factory.StartNew(StartExecutingWork, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
|
||||
// WinUI 3
|
||||
_ = Task.Run(() => StartExecutingWorkAsync());
|
||||
```
|
||||
|
||||
## SoftwareBitmap as Interface Type
|
||||
|
||||
When modules expose imaging interfaces (e.g., AI super-resolution), change parameter/return types:
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
|
||||
|
||||
// WinRT
|
||||
SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath);
|
||||
```
|
||||
|
||||
This eliminates manual `BitmapSource ↔ SoftwareBitmap` conversion code (unsafe `IMemoryBufferByteAccess` COM interop).
|
||||
|
||||
## MultiFrame Image Handling
|
||||
|
||||
```csharp
|
||||
// WinRT multi-frame encode (e.g., multi-page TIFF, animated GIF)
|
||||
for (uint i = 0; i < decoder.FrameCount; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
await encoder.GoToNextFrameAsync();
|
||||
|
||||
var frame = await decoder.GetFrameAsync(i);
|
||||
var bitmap = await frame.GetSoftwareBitmapAsync(
|
||||
frame.BitmapPixelFormat,
|
||||
BitmapAlphaMode.Premultiplied,
|
||||
transform,
|
||||
ExifOrientationMode.IgnoreExifOrientation,
|
||||
ColorManagementMode.DoNotColorManage);
|
||||
encoder.SetSoftwareBitmap(bitmap);
|
||||
}
|
||||
await encoder.FlushAsync();
|
||||
```
|
||||
|
||||
## int → uint for Pixel Dimensions
|
||||
|
||||
WinRT uses `uint` for all pixel dimensions. This affects:
|
||||
- `decoder.PixelWidth` / `decoder.PixelHeight` — `uint`
|
||||
- `BitmapTransform.ScaledWidth` / `ScaledHeight` — `uint`
|
||||
- `SoftwareBitmap` constructor — `uint` parameters
|
||||
- Test assertions: `Assert.AreEqual(96, ...)` → `Assert.AreEqual(96u, ...)`
|
||||
|
||||
## Display SoftwareBitmap in UI
|
||||
|
||||
```csharp
|
||||
var source = new SoftwareBitmapSource();
|
||||
// Must convert to Bgra8/Premultiplied for display
|
||||
if (bitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
|
||||
bitmap.BitmapAlphaMode != BitmapAlphaMode.Premultiplied)
|
||||
{
|
||||
bitmap = SoftwareBitmap.Convert(bitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
|
||||
}
|
||||
await source.SetBitmapAsync(bitmap);
|
||||
myImage.Source = source;
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
| Feature | WPF | WinRT | Impact |
|
||||
|---------|-----|-------|--------|
|
||||
| PNG interlace | `PngBitmapEncoder.Interlace` | Not available | Always non-interlaced |
|
||||
| Metadata stripping | Selective via `BitmapMetadata.Clone()` | All-or-nothing | Orientation EXIF also removed |
|
||||
| Pixel formats | Many (`Pbgra32`, `Bgr24`, `Indexed8`, ...) | Primarily `Bgra8`, `Rgba8`, `Gray8/16` | Convert to `Bgra8` |
|
||||
| WMP/HDP format | `WmpBitmapDecoder` | Not available | Not supported |
|
||||
| Pixel differences | WPF scaler | `BitmapInterpolationMode.Fant` | Not bit-identical |
|
||||
226
.github/skills/wpf-to-winui3-migration/references/namespace-api-mapping.md
vendored
Normal file
226
.github/skills/wpf-to-winui3-migration/references/namespace-api-mapping.md
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
# Namespace and API Mapping Reference
|
||||
|
||||
Complete reference for mapping WPF types to WinUI 3 equivalents, based on the ImageResizer migration.
|
||||
|
||||
## Root Namespace Mapping
|
||||
|
||||
| WPF Namespace | WinUI 3 Namespace |
|
||||
|---------------|-------------------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` |
|
||||
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` |
|
||||
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
|
||||
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
|
||||
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
|
||||
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
|
||||
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` |
|
||||
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` |
|
||||
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` |
|
||||
|
||||
## Core Type Mapping
|
||||
|
||||
| WPF Type | WinUI 3 Type |
|
||||
|----------|-------------|
|
||||
| `System.Windows.Application` | `Microsoft.UI.Xaml.Application` |
|
||||
| `System.Windows.Window` | `Microsoft.UI.Xaml.Window` (NOT a DependencyObject) |
|
||||
| `System.Windows.DependencyObject` | `Microsoft.UI.Xaml.DependencyObject` |
|
||||
| `System.Windows.DependencyProperty` | `Microsoft.UI.Xaml.DependencyProperty` |
|
||||
| `System.Windows.FrameworkElement` | `Microsoft.UI.Xaml.FrameworkElement` |
|
||||
| `System.Windows.UIElement` | `Microsoft.UI.Xaml.UIElement` |
|
||||
| `System.Windows.Visibility` | `Microsoft.UI.Xaml.Visibility` |
|
||||
| `System.Windows.Thickness` | `Microsoft.UI.Xaml.Thickness` |
|
||||
| `System.Windows.CornerRadius` | `Microsoft.UI.Xaml.CornerRadius` |
|
||||
| `System.Windows.Media.Color` | `Windows.UI.Color` (note: `Windows.UI`, not `Microsoft.UI`) |
|
||||
| `System.Windows.Media.Colors` | `Microsoft.UI.Colors` |
|
||||
|
||||
## Controls Mapping
|
||||
|
||||
### Direct Mapping (namespace-only change)
|
||||
|
||||
These controls exist in both frameworks with the same name — change `System.Windows.Controls` to `Microsoft.UI.Xaml.Controls`:
|
||||
|
||||
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
|
||||
|
||||
### Controls With Different Names or Behavior
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `MessageBox` | `ContentDialog` | Must set `XamlRoot` before `ShowAsync()` |
|
||||
| `ContextMenu` | `MenuFlyout` | Different API surface |
|
||||
| `TabControl` | `TabView` | Different API |
|
||||
| `Menu` | `MenuBar` | Different API |
|
||||
| `StatusBar` | Custom `StackPanel` layout | No built-in equivalent |
|
||||
| `AccessText` | Not available | Use `AccessKey` property on target control |
|
||||
|
||||
### WPF-UI (Lepo) to Native WinUI 3
|
||||
|
||||
ImageResizer used the `WPF-UI` library (Lepo) for Fluent styling. These must be replaced with native WinUI 3 equivalents:
|
||||
|
||||
| WPF-UI (Lepo) | WinUI 3 Native | Notes |
|
||||
|----------------|---------------|-------|
|
||||
| `<ui:FluentWindow>` | `<Window>` | Native window + `ExtendsContentIntoTitleBar` |
|
||||
| `<ui:Button>` | `<Button>` | Native button |
|
||||
| `<ui:NumberBox>` | `<NumberBox>` | Built into WinUI 3 |
|
||||
| `<ui:ProgressRing>` | `<ProgressRing>` | Built into WinUI 3 |
|
||||
| `<ui:SymbolIcon>` | `<SymbolIcon>` or `<FontIcon>` | Built into WinUI 3 |
|
||||
| `<ui:InfoBar>` | `<InfoBar>` | Built into WinUI 3 |
|
||||
| `<ui:TitleBar>` | Custom title bar via `SetTitleBar()` | Use `ExtendsContentIntoTitleBar` |
|
||||
| `<ui:ThemesDictionary>` | `<XamlControlsResources>` | In merged dictionaries |
|
||||
| `<ui:ControlsDictionary>` | Remove | Not needed — WinUI 3 has its own control styles |
|
||||
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` | Named style keys |
|
||||
|
||||
## Input Event Mapping
|
||||
|
||||
| WPF Event | WinUI 3 Event | Notes |
|
||||
|-----------|--------------|-------|
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Check `IsLeftButtonPressed` on args |
|
||||
| `MouseLeftButtonUp` | `PointerReleased` | Check pointer properties |
|
||||
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` with right button check |
|
||||
| `MouseMove` | `PointerMoved` | Uses `PointerRoutedEventArgs` |
|
||||
| `MouseWheel` | `PointerWheelChanged` | Different event args |
|
||||
| `MouseEnter` | `PointerEntered` | |
|
||||
| `MouseLeave` | `PointerExited` | |
|
||||
| `MouseDoubleClick` | `DoubleTapped` | Different event args |
|
||||
| `KeyDown` | `KeyDown` | Same name, args type: `KeyRoutedEventArgs` |
|
||||
| `PreviewKeyDown` | No direct equivalent | Use `KeyDown` with handled pattern |
|
||||
|
||||
## IValueConverter Signature Change
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `Convert(object value, Type targetType, object parameter, CultureInfo culture)` | `Convert(object value, Type targetType, object parameter, string language)` |
|
||||
| `ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)` | `ConvertBack(object value, Type targetType, object parameter, string language)` |
|
||||
|
||||
Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All converter classes must be updated.
|
||||
|
||||
## Types That Moved to Different Hierarchies
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `System.Windows.Threading.Dispatcher` | `Microsoft.UI.Dispatching.DispatcherQueue` | Completely different API |
|
||||
| `System.Windows.Threading.DispatcherPriority` | `Microsoft.UI.Dispatching.DispatcherQueuePriority` | Only 3 levels: High/Normal/Low |
|
||||
| `System.Windows.Interop.HwndSource` | `WinRT.Interop.WindowNative` | For HWND interop |
|
||||
| `System.Windows.Interop.WindowInteropHelper` | `WinRT.Interop.WindowNative.GetWindowHandle()` | |
|
||||
| `System.Windows.SystemColors` | Resource keys via `ThemeResource` | No direct static class |
|
||||
| `System.Windows.SystemParameters` | Win32 API or `DisplayInformation` | No direct equivalent |
|
||||
|
||||
## NuGet Package Migration
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| Built into .NET (no NuGet needed) | `Microsoft.WindowsAppSDK` | Required |
|
||||
| `PresentationCore` / `PresentationFramework` | `Microsoft.WinUI` (transitive) | |
|
||||
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` | |
|
||||
| `WPF-UI` (Lepo) | **Remove** — use native WinUI 3 controls | |
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) | |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` | |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` | Required |
|
||||
| (none) | `WinUIEx` | Optional, window helpers |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Extensions` | Optional |
|
||||
| (none) | `Microsoft.Web.WebView2` | If using WebView |
|
||||
|
||||
## Project File Changes
|
||||
|
||||
### WPF .csproj
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### WinUI 3 .csproj
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<SelfContained>true</SelfContained>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Assets\ImageResizer\ImageResizer.ico</ApplicationIcon>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
<ProjectPriFileName>PowerToys.ModuleName.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `UseWPF` → `UseWinUI`
|
||||
- TFM: `net8.0-windows` → `net8.0-windows10.0.19041.0`
|
||||
- Add `WindowsPackageType=None` for unpackaged desktop apps
|
||||
- Add `SelfContained=true` + `WindowsAppSDKSelfContained=true`
|
||||
- Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs` entry point
|
||||
- Set `ProjectPriFileName` to match your module's assembly name
|
||||
- Move icon from `Resources/` to `Assets/<Module>/`
|
||||
|
||||
### XAML ApplicationDefinition Setup
|
||||
|
||||
WinUI 3 requires explicit `ApplicationDefinition` declaration:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<Page Remove="ImageResizerXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="ImageResizerXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### CsWinRT Interop (for GPO and native references)
|
||||
|
||||
If the module references native C++ projects (like `GPOWrapper`):
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
Change `GPOWrapperProjection.csproj` reference to direct `GPOWrapper.vcxproj` reference.
|
||||
|
||||
### InternalsVisibleTo Migration
|
||||
|
||||
Move from code file to `.csproj`:
|
||||
|
||||
```csharp
|
||||
// DELETE: Properties/InternalsVisibleTo.cs
|
||||
// [assembly: InternalsVisibleTo("ImageResizer.Test")]
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- ADD to .csproj: -->
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ImageResizer.Test" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### Items to Remove from .csproj
|
||||
|
||||
```xml
|
||||
<!-- DELETE: WPF resource embedding -->
|
||||
<EmbeddedResource Update="Properties\Resources.resx">...</EmbeddedResource>
|
||||
<Resource Include="Resources\ImageResizer.ico" />
|
||||
<Compile Update="Properties\Resources.Designer.cs">...</Compile>
|
||||
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" /> <!-- from CLI project -->
|
||||
```
|
||||
516
.github/skills/wpf-to-winui3-migration/references/powertoys-patterns.md
vendored
Normal file
516
.github/skills/wpf-to-winui3-migration/references/powertoys-patterns.md
vendored
Normal file
@@ -0,0 +1,516 @@
|
||||
# PowerToys-Specific Migration Patterns
|
||||
|
||||
Patterns and conventions specific to the PowerToys codebase, based on the ImageResizer migration.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Before (WPF Module)
|
||||
|
||||
```
|
||||
src/modules/<module>/
|
||||
├── <Module>UI/
|
||||
│ ├── <Module>UI.csproj # OutputType=WinExe, UseWPF=true
|
||||
│ ├── App.xaml / App.xaml.cs
|
||||
│ ├── MainWindow.xaml / .cs
|
||||
│ ├── Views/
|
||||
│ ├── ViewModels/
|
||||
│ ├── Helpers/
|
||||
│ │ ├── Observable.cs # Custom INotifyPropertyChanged
|
||||
│ │ └── RelayCommand.cs # Custom ICommand
|
||||
│ ├── Properties/
|
||||
│ │ ├── Resources.resx # WPF resource strings
|
||||
│ │ ├── Resources.Designer.cs
|
||||
│ │ └── InternalsVisibleTo.cs
|
||||
│ └── Telemetry/
|
||||
├── <Module>CLI/
|
||||
│ └── <Module>CLI.csproj # OutputType=Exe
|
||||
└── tests/
|
||||
```
|
||||
|
||||
### After (WinUI 3 Module)
|
||||
|
||||
```
|
||||
src/modules/<module>/
|
||||
├── <Module>UI/
|
||||
│ ├── <Module>UI.csproj # OutputType=WinExe, UseWinUI=true
|
||||
│ ├── Program.cs # Custom entry point (DISABLE_XAML_GENERATED_MAIN)
|
||||
│ ├── app.manifest # Single manifest file
|
||||
│ ├── ImageResizerXAML/
|
||||
│ │ ├── App.xaml / App.xaml.cs # WinUI 3 App class
|
||||
│ │ ├── MainWindow.xaml / .cs
|
||||
│ │ └── Views/
|
||||
│ ├── Converters/ # WinUI 3 IValueConverter (string language)
|
||||
│ ├── ViewModels/
|
||||
│ ├── Helpers/
|
||||
│ │ └── ResourceLoaderInstance.cs # Static ResourceLoader accessor
|
||||
│ ├── Utilities/
|
||||
│ │ └── CodecHelper.cs # WPF→WinRT codec ID mapping (if imaging)
|
||||
│ ├── Models/
|
||||
│ │ └── ImagingEnums.cs # Custom enums replacing WPF imaging enums
|
||||
│ ├── Strings/
|
||||
│ │ └── en-us/
|
||||
│ │ └── Resources.resw # WinUI 3 resource strings
|
||||
│ └── Assets/
|
||||
│ └── <Module>/
|
||||
│ └── <Module>.ico # Moved from Resources/
|
||||
├── <Module>Common/ # NEW: shared library for CLI
|
||||
│ └── <Module>Common.csproj # OutputType=Library
|
||||
├── <Module>CLI/
|
||||
│ └── <Module>CLI.csproj # References Common, NOT UI
|
||||
└── tests/
|
||||
```
|
||||
|
||||
### Critical: CLI Dependency Pattern
|
||||
|
||||
**Do NOT** create `ProjectReference` from Exe to WinExe. This causes phantom build artifacts (`.exe`, `.deps.json`, `.runtimeconfig.json`) in the root output directory.
|
||||
|
||||
```
|
||||
WRONG: ImageResizerCLI (Exe) → ImageResizerUI (WinExe) ← phantom artifacts
|
||||
CORRECT: ImageResizerCLI (Exe) → ImageResizerCommon (Library)
|
||||
ImageResizerUI (WinExe) → ImageResizerCommon (Library)
|
||||
```
|
||||
|
||||
Follow the `FancyZonesCLI` → `FancyZonesEditorCommon` pattern.
|
||||
|
||||
### Files to Delete
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `Properties/Resources.resx` | Replaced by `Strings/en-us/Resources.resw` |
|
||||
| `Properties/Resources.Designer.cs` | Auto-generated; no longer needed |
|
||||
| `Properties/InternalsVisibleTo.cs` | Moved to `.csproj` `<InternalsVisibleTo>` |
|
||||
| `Helpers/Observable.cs` | Replaced by `CommunityToolkit.Mvvm.ObservableObject` |
|
||||
| `Helpers/RelayCommand.cs` | Replaced by `CommunityToolkit.Mvvm.Input` |
|
||||
| `Resources/*.ico` / `Resources/*.png` | Moved to `Assets/<Module>/` |
|
||||
| WPF `.dev.manifest` / `.prod.manifest` | Replaced by single `app.manifest` |
|
||||
| WPF-specific converters | Replaced by WinUI 3 converters with `string language` |
|
||||
|
||||
---
|
||||
|
||||
## MVVM Migration: Custom → CommunityToolkit.Mvvm Source Generators
|
||||
|
||||
### Observable Base Class → ObservableObject + [ObservableProperty]
|
||||
|
||||
**Before (custom Observable):**
|
||||
```csharp
|
||||
public class ResizeSize : Observable
|
||||
{
|
||||
private int _id;
|
||||
public int Id { get => _id; set => Set(ref _id, value); }
|
||||
|
||||
private ResizeFit _fit;
|
||||
public ResizeFit Fit
|
||||
{
|
||||
get => _fit;
|
||||
set
|
||||
{
|
||||
Set(ref _fit, value);
|
||||
UpdateShowHeight();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _showHeight = true;
|
||||
public bool ShowHeight { get => _showHeight; set => Set(ref _showHeight, value); }
|
||||
private void UpdateShowHeight() { ShowHeight = Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent; }
|
||||
}
|
||||
```
|
||||
|
||||
**After (CommunityToolkit.Mvvm source generators):**
|
||||
```csharp
|
||||
public partial class ResizeSize : ObservableObject // MUST be partial
|
||||
{
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("Id")]
|
||||
private int _id;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowHeight))] // Replaces manual UpdateShowHeight()
|
||||
private ResizeFit _fit;
|
||||
|
||||
// Computed property — no backing field, no manual update method
|
||||
public bool ShowHeight => Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- Class must be `partial` for source generators
|
||||
- `Observable` → `ObservableObject` (from CommunityToolkit.Mvvm)
|
||||
- Manual `Set(ref _field, value)` → `[ObservableProperty]` attribute
|
||||
- `PropertyChanged` dependencies → `[NotifyPropertyChangedFor(nameof(...))]`
|
||||
- Computed properties with manual `UpdateXxx()` → direct expression body
|
||||
|
||||
### Custom Name Setter with Transform
|
||||
|
||||
For properties that transform the value before storing:
|
||||
|
||||
```csharp
|
||||
// Cannot use [ObservableProperty] because of value transformation
|
||||
private string _name;
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, ReplaceTokens(value)); // SetProperty from ObservableObject
|
||||
}
|
||||
```
|
||||
|
||||
### RelayCommand → [RelayCommand] Source Generator
|
||||
|
||||
```csharp
|
||||
// DELETE: Helpers/RelayCommand.cs (custom ICommand)
|
||||
|
||||
// Before
|
||||
public ICommand ResizeCommand { get; } = new RelayCommand(Execute);
|
||||
|
||||
// After
|
||||
[RelayCommand]
|
||||
private void Resize() { /* ... */ }
|
||||
// Source generator creates ResizeCommand property automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resource String Migration (.resx → .resw)
|
||||
|
||||
### ResourceLoaderInstance Helper
|
||||
|
||||
```csharp
|
||||
internal static class ResourceLoaderInstance
|
||||
{
|
||||
internal static ResourceLoader ResourceLoader { get; private set; }
|
||||
|
||||
static ResourceLoaderInstance()
|
||||
{
|
||||
ResourceLoader = new ResourceLoader("PowerToys.ImageResizer.pri");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Use the single-argument `ResourceLoader` constructor. The two-argument version (`ResourceLoader("file.pri", "path/Resources")`) may fail if the resource map path doesn't match the actual PRI structure.
|
||||
|
||||
### Usage
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
using ImageResizer.Properties;
|
||||
string text = Resources.MyStringKey;
|
||||
|
||||
// WinUI 3
|
||||
string text = ResourceLoaderInstance.ResourceLoader.GetString("MyStringKey");
|
||||
```
|
||||
|
||||
### Lazy Initialization for Resource-Dependent Statics
|
||||
|
||||
`ResourceLoader` is not available at class-load time in all contexts (CLI mode, test harness). Use lazy initialization:
|
||||
|
||||
**Before (crashes at class load):**
|
||||
```csharp
|
||||
private static readonly CompositeFormat _format =
|
||||
CompositeFormat.Parse(Resources.Error_Format);
|
||||
|
||||
private static readonly Dictionary<string, string> _tokens = new()
|
||||
{
|
||||
["$small$"] = Resources.Small,
|
||||
["$medium$"] = Resources.Medium,
|
||||
};
|
||||
```
|
||||
|
||||
**After (lazy, safe):**
|
||||
```csharp
|
||||
private static CompositeFormat _format;
|
||||
private static CompositeFormat Format => _format ??=
|
||||
CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("Error_Format"));
|
||||
|
||||
private static readonly Lazy<Dictionary<string, string>> _tokens = new(() =>
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["$small$"] = ResourceLoaderInstance.ResourceLoader.GetString("Small"),
|
||||
["$medium$"] = ResourceLoaderInstance.ResourceLoader.GetString("Medium"),
|
||||
});
|
||||
// Usage: _tokens.Value.TryGetValue(...)
|
||||
```
|
||||
|
||||
### XAML: x:Static → x:Uid
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
<Button Content="{x:Static p:Resources.Cancel}" />
|
||||
<!-- WinUI 3 -->
|
||||
<Button x:Uid="Cancel" />
|
||||
```
|
||||
|
||||
In `.resw`, use property-suffixed keys: `Cancel.Content`, `Header.Text`, etc.
|
||||
|
||||
---
|
||||
|
||||
## CLI Options Migration
|
||||
|
||||
`System.CommandLine.Option<T>` constructor signature changed:
|
||||
|
||||
```csharp
|
||||
// WPF era — string[] aliases
|
||||
public DestinationOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Destination)
|
||||
|
||||
// WinUI 3 — single string name
|
||||
public DestinationOption()
|
||||
: base(_aliases[0], ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Destination"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installer Updates
|
||||
|
||||
### WiX Changes
|
||||
|
||||
#### 1. Remove Satellite Assembly References
|
||||
|
||||
Remove from `installer/PowerToysSetupVNext/Resources.wxs`:
|
||||
- `<Component>` entries for `<Module>.resources.dll`
|
||||
- `<RemoveFolder>` entries for locale directories
|
||||
- Module from `WinUI3AppsInstallFolder` `ParentDirectory` loop
|
||||
|
||||
#### 2. Update File Component Generation
|
||||
|
||||
Run `generateAllFileComponents.ps1` after migration. For Exe→WinExe dependency issues, add cleanup logic:
|
||||
|
||||
```powershell
|
||||
# Strip phantom ImageResizer files from BaseApplications.wxs
|
||||
$content = $content -replace 'PowerToys\.ImageResizer\.exe', ''
|
||||
$content = $content -replace 'PowerToys\.ImageResizer\.deps\.json', ''
|
||||
$content = $content -replace 'PowerToys\.ImageResizer\.runtimeconfig\.json', ''
|
||||
```
|
||||
|
||||
#### 3. Output Directory
|
||||
|
||||
WinUI 3 modules output to `WinUI3Apps/`:
|
||||
```xml
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
|
||||
```
|
||||
|
||||
### ESRP Signing
|
||||
|
||||
Update `.pipelines/ESRPSigning_core.json` — all module binaries must use `WinUI3Apps\\` paths:
|
||||
|
||||
```json
|
||||
{
|
||||
"FileList": [
|
||||
"WinUI3Apps\\PowerToys.ImageResizer.exe",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerExt.dll",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerContextMenu.dll"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Pipeline Fixes
|
||||
|
||||
### $(SolutionDir) → $(MSBuildThisFileDirectory)
|
||||
|
||||
`$(SolutionDir)` is empty when building individual projects outside the solution. Replace with relative paths from the project file:
|
||||
|
||||
```xml
|
||||
<!-- Before (breaks on standalone project build) -->
|
||||
<Exec Command="powershell $(SolutionDir)tools\build\convert-resx-to-rc.ps1" />
|
||||
|
||||
<!-- After (works always) -->
|
||||
<Exec Command="powershell $(MSBuildThisFileDirectory)..\..\..\..\tools\build\convert-resx-to-rc.ps1" />
|
||||
```
|
||||
|
||||
### MSIX Packaging: PreBuild → PostBuild
|
||||
|
||||
MSIX packaging must happen AFTER the build (artifacts not ready at PreBuild):
|
||||
|
||||
```xml
|
||||
<!-- Before -->
|
||||
<PreBuildEvent>MakeAppx.exe pack /d . /p "$(OutDir)Package.msix" /o</PreBuildEvent>
|
||||
|
||||
<!-- After -->
|
||||
<PostBuildEvent>
|
||||
if exist "$(OutDir)Package.msix" del "$(OutDir)Package.msix"
|
||||
MakeAppx.exe pack /d "$(MSBuildThisFileDirectory)." /p "$(OutDir)Package.msix" /o
|
||||
</PostBuildEvent>
|
||||
```
|
||||
|
||||
### RC File Icon Path Escaping
|
||||
|
||||
Windows Resource Compiler requires double-backslash paths:
|
||||
|
||||
```c
|
||||
// Before (breaks)
|
||||
IDI_ICON1 ICON "..\\ui\Assets\ImageResizer\ImageResizer.ico"
|
||||
// After
|
||||
IDI_ICON1 ICON "..\\ui\\Assets\\ImageResizer\\ImageResizer.ico"
|
||||
```
|
||||
|
||||
### BOM/Encoding Normalization
|
||||
|
||||
Migration may strip UTF-8 BOM from C# files (`// Copyright` → `// Copyright`). This is cosmetic and safe, but be aware it will show as changes in diff.
|
||||
|
||||
---
|
||||
|
||||
## Test Adaptation
|
||||
|
||||
### Tests Requiring WPF Runtime
|
||||
|
||||
If tests still need WPF types (e.g., comparing old vs new output), temporarily add:
|
||||
```xml
|
||||
<UseWPF>true</UseWPF>
|
||||
```
|
||||
Remove this after fully migrating all test code to WinRT APIs.
|
||||
|
||||
### Tests Using ResourceLoader
|
||||
|
||||
Unit tests cannot easily initialize WinUI 3 `ResourceLoader`. Options:
|
||||
- Hardcode expected strings in tests: `"Value must be between '{0}' and '{1}'."`
|
||||
- Delete tests that only verify resource string lookup
|
||||
- Avoid creating `App` instances in test harness (WinUI App cannot be instantiated in tests)
|
||||
|
||||
### Async Test Methods
|
||||
|
||||
All imaging tests become async:
|
||||
```csharp
|
||||
// Before
|
||||
[TestMethod]
|
||||
public void ResizesImage() { ... }
|
||||
|
||||
// After
|
||||
[TestMethod]
|
||||
public async Task ResizesImageAsync() { ... }
|
||||
```
|
||||
|
||||
### uint Assertions
|
||||
|
||||
```csharp
|
||||
// Before
|
||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
||||
// After
|
||||
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||
```
|
||||
|
||||
### Pixel Data Access in Tests
|
||||
|
||||
```csharp
|
||||
// Before (WPF)
|
||||
public static Color GetFirstPixel(this BitmapSource source)
|
||||
{
|
||||
var pixel = new byte[4];
|
||||
new FormatConvertedBitmap(
|
||||
new CroppedBitmap(source, new Int32Rect(0, 0, 1, 1)),
|
||||
PixelFormats.Bgra32, null, 0).CopyPixels(pixel, 4, 0);
|
||||
return Color.FromArgb(pixel[3], pixel[2], pixel[1], pixel[0]);
|
||||
}
|
||||
|
||||
// After (WinRT)
|
||||
public static async Task<(byte R, byte G, byte B, byte A)> GetFirstPixelAsync(
|
||||
this BitmapDecoder decoder)
|
||||
{
|
||||
using var bitmap = await decoder.GetSoftwareBitmapAsync(
|
||||
BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
|
||||
var buffer = new Windows.Storage.Streams.Buffer(
|
||||
(uint)(bitmap.PixelWidth * bitmap.PixelHeight * 4));
|
||||
bitmap.CopyToBuffer(buffer);
|
||||
using var reader = DataReader.FromBuffer(buffer);
|
||||
byte b = reader.ReadByte(), g = reader.ReadByte(),
|
||||
r = reader.ReadByte(), a = reader.ReadByte();
|
||||
return (r, g, b, a);
|
||||
}
|
||||
```
|
||||
|
||||
### Metadata Assertions
|
||||
|
||||
```csharp
|
||||
// Before
|
||||
Assert.AreEqual("Test", ((BitmapMetadata)image.Frames[0].Metadata).Comment);
|
||||
|
||||
// After
|
||||
var props = await decoder.BitmapProperties.GetPropertiesAsync(
|
||||
new[] { "System.Photo.DateTaken" });
|
||||
Assert.IsTrue(props.ContainsKey("System.Photo.DateTaken"),
|
||||
"Metadata should be preserved during transcode");
|
||||
```
|
||||
|
||||
### AllowUnsafeBlocks for SoftwareBitmap Tests
|
||||
|
||||
If tests access pixel data via `IMemoryBufferByteAccess`, add:
|
||||
```xml
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Settings JSON Backward Compatibility
|
||||
|
||||
- Settings are stored in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
|
||||
- Schema must remain backward-compatible across upgrades
|
||||
- Add new fields with defaults; never remove or rename existing fields
|
||||
- Create custom enums matching WPF enum integer values for deserialization (e.g., `ImagingEnums.cs`)
|
||||
- See: `src/settings-ui/Settings.UI.Library/`
|
||||
|
||||
## IPC Contract
|
||||
|
||||
If the module communicates with the runner or settings UI:
|
||||
1. Update BOTH sides of the IPC contract
|
||||
2. Test settings changes are received by the module
|
||||
3. Test module state changes are reflected in settings UI
|
||||
4. Reference: `doc/devdocs/core/settings/runner-ipc.md`
|
||||
|
||||
---
|
||||
|
||||
## Checklist for PowerToys Module Migration
|
||||
|
||||
### Project & Dependencies
|
||||
- [ ] Update `.csproj`: `UseWPF` → `UseWinUI`, TFM → `net8.0-windows10.0.19041.0`
|
||||
- [ ] Add `WindowsPackageType=None`, `SelfContained=true`, `WindowsAppSDKSelfContained=true`
|
||||
- [ ] Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs`
|
||||
- [ ] Replace NuGet packages (WPF-UI → remove, add WindowsAppSDK, etc.)
|
||||
- [ ] Update project references (GPOWrapperProjection → GPOWrapper + CsWinRT)
|
||||
- [ ] Move `InternalsVisibleTo` from code to `.csproj`
|
||||
- [ ] Extract CLI shared logic to Library project (avoid Exe→WinExe dependency)
|
||||
|
||||
### MVVM & Resources
|
||||
- [ ] Replace custom `Observable`/`RelayCommand` with CommunityToolkit.Mvvm source generators
|
||||
- [ ] Migrate `.resx` → `.resw` (`Properties/Resources.resx` → `Strings/en-us/Resources.resw`)
|
||||
- [ ] Create `ResourceLoaderInstance` helper
|
||||
- [ ] Wrap resource-dependent statics in `Lazy<T>` or null-coalescing properties
|
||||
- [ ] Delete `Properties/Resources.Designer.cs`, `Observable.cs`, `RelayCommand.cs`
|
||||
|
||||
### XAML
|
||||
- [ ] Replace `clr-namespace:` → `using:` in all xmlns declarations
|
||||
- [ ] Remove WPF-UI (Lepo) xmlns and controls — use native WinUI 3
|
||||
- [ ] Replace `{x:Static p:Resources.Key}` → `x:Uid` with `.resw` keys
|
||||
- [ ] Replace `{DynamicResource}` → `{ThemeResource}`
|
||||
- [ ] Replace `DataType="{x:Type ...}"` → `x:DataType="..."`
|
||||
- [ ] Replace `<Style.Triggers>` → `VisualStateManager`
|
||||
- [ ] Add `<XamlControlsResources/>` to `App.xaml` merged dictionaries
|
||||
- [ ] Move `Window.Resources` to root container's `Resources`
|
||||
- [ ] Run XamlStyler: `.\.pipelines\applyXamlStyling.ps1 -Main`
|
||||
|
||||
### Code-Behind & APIs
|
||||
- [ ] Replace all `System.Windows.*` namespaces with `Microsoft.UI.Xaml.*`
|
||||
- [ ] Replace `Dispatcher` with `DispatcherQueue`
|
||||
- [ ] Store `DispatcherQueue` reference explicitly (no `Application.Current.Dispatcher`)
|
||||
- [ ] Implement `SizeToContent()` via AppWindow if needed
|
||||
- [ ] Update `ContentDialog` calls to set `XamlRoot`
|
||||
- [ ] Update `FilePicker` calls with HWND initialization
|
||||
- [ ] Migrate imaging code to `Windows.Graphics.Imaging` (async, `SoftwareBitmap`)
|
||||
- [ ] Create `CodecHelper` for legacy GUID → WinRT codec ID mapping (if imaging)
|
||||
- [ ] Create custom imaging enums for JSON backward compatibility (if imaging)
|
||||
- [ ] Update all `IValueConverter` signatures (`CultureInfo` → `string`)
|
||||
|
||||
### Build & Installer
|
||||
- [ ] Update WiX installer: remove satellite assembly refs from `Resources.wxs`
|
||||
- [ ] Run `generateAllFileComponents.ps1`; handle phantom artifacts
|
||||
- [ ] Update ESRP signing paths to `WinUI3Apps\\`
|
||||
- [ ] Fix `$(SolutionDir)` → `$(MSBuildThisFileDirectory)` in build events
|
||||
- [ ] Move MSIX packaging from PreBuild to PostBuild
|
||||
- [ ] Fix RC file path escaping (double-backslash)
|
||||
- [ ] Verify output dir is `WinUI3Apps/`
|
||||
|
||||
### Testing & Validation
|
||||
- [ ] Update test project: async methods, `uint` assertions
|
||||
- [ ] Handle ResourceLoader unavailability in tests (hardcode strings or skip)
|
||||
- [ ] Build clean: `cd` to project folder, `tools/build/build.cmd`, exit code 0
|
||||
- [ ] Run tests for affected module
|
||||
- [ ] Verify settings JSON backward compatibility
|
||||
- [ ] Test IPC contracts (runner ↔ settings UI)
|
||||
314
.github/skills/wpf-to-winui3-migration/references/threading-and-windowing.md
vendored
Normal file
314
.github/skills/wpf-to-winui3-migration/references/threading-and-windowing.md
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
# Threading and Window Management Migration
|
||||
|
||||
Based on patterns from the ImageResizer migration.
|
||||
|
||||
## Dispatcher → DispatcherQueue
|
||||
|
||||
### API Mapping
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `Dispatcher.Invoke(Action)` | `DispatcherQueue.TryEnqueue(Action)` |
|
||||
| `Dispatcher.BeginInvoke(Action)` | `DispatcherQueue.TryEnqueue(Action)` |
|
||||
| `Dispatcher.Invoke(DispatcherPriority, Action)` | `DispatcherQueue.TryEnqueue(DispatcherQueuePriority, Action)` |
|
||||
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` |
|
||||
| `Dispatcher.VerifyAccess()` | Check `DispatcherQueue.HasThreadAccess` (no exception-throwing method) |
|
||||
|
||||
### Priority Mapping
|
||||
|
||||
WinUI 3 has only 3 levels: `High`, `Normal`, `Low`.
|
||||
|
||||
| WPF `DispatcherPriority` | WinUI 3 `DispatcherQueuePriority` |
|
||||
|-------------------------|----------------------------------|
|
||||
| `Send` | `High` |
|
||||
| `Normal` / `Input` / `Loaded` / `Render` / `DataBind` | `Normal` |
|
||||
| `Background` / `ContextIdle` / `ApplicationIdle` / `SystemIdle` | `Low` |
|
||||
|
||||
### Pattern: Global DispatcherQueue Access (from ImageResizer)
|
||||
|
||||
WPF provided `Application.Current.Dispatcher` globally. WinUI 3 requires explicit storage:
|
||||
|
||||
```csharp
|
||||
// Store DispatcherQueue at app startup
|
||||
private static DispatcherQueue _uiDispatcherQueue;
|
||||
|
||||
public static void InitializeDispatcher()
|
||||
{
|
||||
_uiDispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
```
|
||||
|
||||
Usage with thread-check pattern (from `Settings.Reload()`):
|
||||
```csharp
|
||||
var currentDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
if (currentDispatcher != null)
|
||||
{
|
||||
// Already on UI thread
|
||||
ReloadCore(jsonSettings);
|
||||
}
|
||||
else if (_uiDispatcherQueue != null)
|
||||
{
|
||||
// Dispatch to UI thread
|
||||
_uiDispatcherQueue.TryEnqueue(() => ReloadCore(jsonSettings));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback (e.g., CLI mode, no UI)
|
||||
ReloadCore(jsonSettings);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: DispatcherQueue in ViewModels (from ProgressViewModel)
|
||||
|
||||
```csharp
|
||||
public class ProgressViewModel
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
public ProgressViewModel()
|
||||
{
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
|
||||
private void OnProgressChanged(double progress)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
Progress = progress;
|
||||
// other UI updates...
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Async Dispatch (await)
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
await this.Dispatcher.InvokeAsync(() => { /* UI work */ });
|
||||
|
||||
// WinUI 3 (using TaskCompletionSource)
|
||||
var tcs = new TaskCompletionSource();
|
||||
this.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
try { /* UI work */ tcs.SetResult(); }
|
||||
catch (Exception ex) { tcs.SetException(ex); }
|
||||
});
|
||||
await tcs.Task;
|
||||
```
|
||||
|
||||
### C++/WinRT Threading
|
||||
|
||||
| Old API | New API |
|
||||
|---------|---------|
|
||||
| `winrt::resume_foreground(CoreDispatcher)` | `wil::resume_foreground(DispatcherQueue)` |
|
||||
| `CoreDispatcher.RunAsync()` | `DispatcherQueue.TryEnqueue()` |
|
||||
|
||||
Add `Microsoft.Windows.ImplementationLibrary` NuGet for `wil::resume_foreground`.
|
||||
|
||||
---
|
||||
|
||||
## Window Management
|
||||
|
||||
### WPF Window vs WinUI 3 Window
|
||||
|
||||
| Feature | WPF `Window` | WinUI 3 `Window` |
|
||||
|---------|-------------|------------------|
|
||||
| Base class | `ContentControl` → `DependencyObject` | **NOT** a control, NOT a `DependencyObject` |
|
||||
| `Resources` property | Yes | No — use root container's `Resources` |
|
||||
| `DataContext` property | Yes | No — use root `Page`/`UserControl` |
|
||||
| `VisualStateManager` | Yes | No — use inside child controls |
|
||||
| `Load`/`Unload` events | Yes | No |
|
||||
| `SizeToContent` | Yes (`Height`/`Width`/`WidthAndHeight`) | No — must implement manually |
|
||||
| `WindowState` (min/max/normal) | Yes | No — use `AppWindow.Presenter` |
|
||||
| `WindowStyle` | Yes | No — use `AppWindow` title bar APIs |
|
||||
| `ResizeMode` | Yes | No — use `AppWindow.Presenter` |
|
||||
| `WindowStartupLocation` | Yes | No — calculate manually |
|
||||
| `Icon` | `Window.Icon` | `AppWindow.SetIcon()` |
|
||||
| `Title` | `Window.Title` | `AppWindow.Title` (or `Window.Title`) |
|
||||
| Size (Width/Height) | Yes | No — use `AppWindow.Resize()` |
|
||||
| Position (Left/Top) | Yes | No — use `AppWindow.Move()` |
|
||||
| `IsDefault`/`IsCancel` on buttons | Yes | No — handle Enter/Escape in code-behind |
|
||||
|
||||
### Getting AppWindow from Window
|
||||
|
||||
```csharp
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using WinRT.Interop;
|
||||
|
||||
IntPtr hwnd = WindowNative.GetWindowHandle(window);
|
||||
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
```
|
||||
|
||||
### Pattern: SizeToContent Replacement (from ImageResizer)
|
||||
|
||||
WinUI 3 has no `SizeToContent`. ImageResizer implemented a manual equivalent:
|
||||
|
||||
```csharp
|
||||
private void SizeToContent()
|
||||
{
|
||||
if (Content is not FrameworkElement content)
|
||||
return;
|
||||
|
||||
// Measure desired content size
|
||||
content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var desiredHeight = content.DesiredSize.Height + WindowChromeHeight + Padding;
|
||||
|
||||
// Account for DPI scaling
|
||||
var scaleFactor = Content.XamlRoot.RasterizationScale;
|
||||
var pixelHeight = (int)(desiredHeight * scaleFactor);
|
||||
var pixelWidth = (int)(WindowWidth * scaleFactor);
|
||||
|
||||
// Resize via AppWindow
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
var appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
appWindow.Resize(new Windows.Graphics.SizeInt32(pixelWidth, pixelHeight));
|
||||
}
|
||||
```
|
||||
|
||||
**Key details:**
|
||||
- `WindowChromeHeight` ≈ 32px for the title bar
|
||||
- Must multiply by `RasterizationScale` for DPI-aware sizing
|
||||
- Call `SizeToContent()` after page navigation or content changes
|
||||
- Unsubscribe previous event handlers before subscribing new ones to avoid memory leaks
|
||||
|
||||
### Window Positioning (Center Screen)
|
||||
|
||||
```csharp
|
||||
var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest);
|
||||
var centerX = (displayArea.WorkArea.Width - appWindow.Size.Width) / 2;
|
||||
var centerY = (displayArea.WorkArea.Height - appWindow.Size.Height) / 2;
|
||||
appWindow.Move(new Windows.Graphics.PointInt32(centerX, centerY));
|
||||
```
|
||||
|
||||
### Window State (Minimize/Maximize)
|
||||
|
||||
```csharp
|
||||
(appWindow.Presenter as OverlappedPresenter)?.Maximize();
|
||||
(appWindow.Presenter as OverlappedPresenter)?.Minimize();
|
||||
(appWindow.Presenter as OverlappedPresenter)?.Restore();
|
||||
```
|
||||
|
||||
### Title Bar Customization
|
||||
|
||||
```csharp
|
||||
// Extend content into title bar
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.SetTitleBar(AppTitleBar); // AppTitleBar is a XAML element
|
||||
|
||||
// Or via AppWindow API
|
||||
if (AppWindowTitleBar.IsCustomizationSupported())
|
||||
{
|
||||
var titleBar = appWindow.TitleBar;
|
||||
titleBar.ExtendsContentIntoTitleBar = true;
|
||||
titleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
}
|
||||
```
|
||||
|
||||
### Tracking the Main Window
|
||||
|
||||
```csharp
|
||||
public partial class App : Application
|
||||
{
|
||||
public static Window MainWindow { get; private set; }
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
MainWindow = new MainWindow();
|
||||
MainWindow.Activate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ContentDialog Requires XamlRoot
|
||||
|
||||
```csharp
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = "Confirm",
|
||||
Content = "Are you sure?",
|
||||
PrimaryButtonText = "Yes",
|
||||
CloseButtonText = "No",
|
||||
XamlRoot = this.Content.XamlRoot // REQUIRED
|
||||
};
|
||||
var result = await dialog.ShowAsync();
|
||||
```
|
||||
|
||||
### File Pickers Require HWND
|
||||
|
||||
```csharp
|
||||
var picker = new FileOpenPicker();
|
||||
picker.FileTypeFilter.Add(".jpg");
|
||||
|
||||
// REQUIRED for desktop apps
|
||||
var hwnd = WindowNative.GetWindowHandle(App.MainWindow);
|
||||
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
|
||||
|
||||
var file = await picker.PickSingleFileAsync();
|
||||
```
|
||||
|
||||
### Window Close Handling
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
protected override void OnClosing(CancelEventArgs e) { e.Cancel = true; this.Hide(); }
|
||||
|
||||
// WinUI 3
|
||||
this.AppWindow.Closing += (s, e) => { e.Cancel = true; this.AppWindow.Hide(); };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Entry Point (DISABLE_XAML_GENERATED_MAIN)
|
||||
|
||||
ImageResizer uses a custom `Program.cs` entry point instead of the WinUI 3 auto-generated `Main`. This is needed for:
|
||||
- CLI mode (process files without showing UI)
|
||||
- Custom initialization before the WinUI 3 App starts
|
||||
- Single-instance enforcement
|
||||
|
||||
### Setup
|
||||
|
||||
In `.csproj`:
|
||||
```xml
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
```
|
||||
|
||||
Create `Program.cs`:
|
||||
```csharp
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0)
|
||||
{
|
||||
// CLI mode — no UI
|
||||
return RunCli(args);
|
||||
}
|
||||
|
||||
// GUI mode
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
Application.Start((p) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(
|
||||
DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WPF App Constructor Removal
|
||||
|
||||
WPF modules often created `new App()` to initialize the WPF `Application` and get `Application.Current.Dispatcher`. This is no longer needed — the WinUI 3 `Application.Start()` handles this.
|
||||
|
||||
```csharp
|
||||
// DELETE (WPF pattern):
|
||||
_imageResizerApp = new App();
|
||||
// REPLACE with: Store DispatcherQueue explicitly (see Global DispatcherQueue Access above)
|
||||
```
|
||||
365
.github/skills/wpf-to-winui3-migration/references/xaml-migration.md
vendored
Normal file
365
.github/skills/wpf-to-winui3-migration/references/xaml-migration.md
vendored
Normal file
@@ -0,0 +1,365 @@
|
||||
# XAML Migration Guide
|
||||
|
||||
Detailed reference for migrating XAML from WPF to WinUI 3, based on the ImageResizer migration.
|
||||
|
||||
## XML Namespace Declaration Changes
|
||||
|
||||
### Before (WPF)
|
||||
|
||||
```xml
|
||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:MyApp"
|
||||
xmlns:m="clr-namespace:ImageResizer.Models"
|
||||
xmlns:p="clr-namespace:ImageResizer.Properties"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
x:Class="MyApp.MainWindow">
|
||||
```
|
||||
|
||||
### After (WinUI 3)
|
||||
|
||||
```xml
|
||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:MyApp"
|
||||
xmlns:m="using:ImageResizer.Models"
|
||||
xmlns:converters="using:ImageResizer.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
x:Class="MyApp.MainWindow">
|
||||
```
|
||||
|
||||
### Key Changes
|
||||
|
||||
| WPF Syntax | WinUI 3 Syntax | Notes |
|
||||
|------------|---------------|-------|
|
||||
| `clr-namespace:Foo` | `using:Foo` | CLR namespace mapping |
|
||||
| `clr-namespace:Foo;assembly=Bar` | `using:Foo` | Assembly qualification not needed |
|
||||
| `xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"` | **Remove entirely** | WPF-UI namespace no longer needed |
|
||||
| `xmlns:p="clr-namespace:...Properties"` | **Remove** | No more `.resx` string bindings |
|
||||
| `sys:String` (from mscorlib) | `x:String` | XAML intrinsic types |
|
||||
| `sys:Int32` | `x:Int32` | XAML intrinsic types |
|
||||
| `sys:Boolean` | `x:Boolean` | XAML intrinsic types |
|
||||
| `sys:Double` | `x:Double` | XAML intrinsic types |
|
||||
|
||||
## Unsupported Markup Extensions
|
||||
|
||||
| WPF Markup Extension | WinUI 3 Alternative |
|
||||
|----------------------|---------------------|
|
||||
| `{DynamicResource Key}` | `{ThemeResource Key}` (theme-reactive) or `{StaticResource Key}` |
|
||||
| `{x:Static Type.Member}` | `{x:Bind}` to a static property, or code-behind |
|
||||
| `{x:Type local:MyType}` | Not supported; use code-behind |
|
||||
| `{x:Array}` | Not supported; create collections in code-behind |
|
||||
| `{x:Code}` | Not supported |
|
||||
|
||||
### DynamicResource → ThemeResource
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
<TextBlock Foreground="{DynamicResource MyBrush}" />
|
||||
|
||||
<!-- WinUI 3 -->
|
||||
<TextBlock Foreground="{ThemeResource MyBrush}" />
|
||||
```
|
||||
|
||||
`ThemeResource` automatically updates when the app theme changes (Light/Dark/HighContrast). For truly dynamic non-theme resources, set values in code-behind or use data binding.
|
||||
|
||||
### x:Static Resource Strings → x:Uid
|
||||
|
||||
This is the most pervasive XAML change. WPF used `{x:Static}` to bind to strongly-typed `.resx` resource strings. WinUI 3 uses `x:Uid` with `.resw` files.
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<Button Content="{x:Static p:Resources.Cancel}" />
|
||||
<TextBlock Text="{x:Static p:Resources.Input_Header}" />
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<Button x:Uid="Cancel" />
|
||||
<TextBlock x:Uid="Input_Header" />
|
||||
```
|
||||
|
||||
In `Strings/en-us/Resources.resw`:
|
||||
```xml
|
||||
<data name="Cancel.Content" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="Input_Header.Text" xml:space="preserve">
|
||||
<value>Select a size</value>
|
||||
</data>
|
||||
```
|
||||
|
||||
The `x:Uid` suffix (`.Content`, `.Text`, `.Header`, `.PlaceholderText`, etc.) matches the target property name.
|
||||
|
||||
### DataType with x:Type → Remove
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<DataTemplate DataType="{x:Type m:ResizeSize}">
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<DataTemplate x:DataType="m:ResizeSize">
|
||||
```
|
||||
|
||||
## WPF-UI (Lepo) Controls Removal
|
||||
|
||||
If the module uses the `WPF-UI` library, replace all Lepo controls with native WinUI 3 equivalents.
|
||||
|
||||
### Window
|
||||
|
||||
```xml
|
||||
<!-- WPF (WPF-UI) -->
|
||||
<ui:FluentWindow
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<ui:TitleBar Title="Image Resizer" />
|
||||
...
|
||||
</ui:FluentWindow>
|
||||
|
||||
<!-- WinUI 3 (native) -->
|
||||
<Window>
|
||||
<!-- Title bar managed via code-behind: this.ExtendsContentIntoTitleBar = true; -->
|
||||
...
|
||||
</Window>
|
||||
```
|
||||
|
||||
### App.xaml Resources
|
||||
|
||||
```xml
|
||||
<!-- WPF (WPF-UI) -->
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemesDictionary Theme="Dark" />
|
||||
<ui:ControlsDictionary />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<!-- WinUI 3 (native) -->
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
### Common Control Replacements
|
||||
|
||||
```xml
|
||||
<!-- WPF-UI NumberBox -->
|
||||
<ui:NumberBox Value="{Binding Width}" />
|
||||
<!-- WinUI 3 -->
|
||||
<NumberBox Value="{x:Bind ViewModel.Width, Mode=TwoWay}" />
|
||||
|
||||
<!-- WPF-UI InfoBar -->
|
||||
<ui:InfoBar Title="Warning" Message="..." IsOpen="True" Severity="Warning" />
|
||||
<!-- WinUI 3 -->
|
||||
<InfoBar Title="Warning" Message="..." IsOpen="True" Severity="Warning" />
|
||||
|
||||
<!-- WPF-UI ProgressRing -->
|
||||
<ui:ProgressRing IsIndeterminate="True" />
|
||||
<!-- WinUI 3 -->
|
||||
<ProgressRing IsActive="True" />
|
||||
|
||||
<!-- WPF-UI SymbolIcon -->
|
||||
<ui:SymbolIcon Symbol="Add" />
|
||||
<!-- WinUI 3 -->
|
||||
<SymbolIcon Symbol="Add" />
|
||||
```
|
||||
|
||||
### Button Patterns
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
<Button IsDefault="True" Content="OK" />
|
||||
<Button IsCancel="True" Content="Cancel" />
|
||||
|
||||
<!-- WinUI 3 (no IsDefault/IsCancel) -->
|
||||
<Button Style="{StaticResource AccentButtonStyle}" Content="OK" />
|
||||
<Button Content="Cancel" />
|
||||
<!-- Handle Enter/Escape keys in code-behind if needed -->
|
||||
```
|
||||
|
||||
## Style and Template Changes
|
||||
|
||||
### Triggers → VisualStateManager
|
||||
|
||||
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<Style TargetType="Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="LightBlue"/>
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="PointerOver">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.Background" Value="LightBlue"/>
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<ContentPresenter />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
```
|
||||
|
||||
### No Binding in Setter.Value
|
||||
|
||||
```xml
|
||||
<!-- WPF (works) -->
|
||||
<Setter Property="Foreground" Value="{Binding TextColor}"/>
|
||||
|
||||
<!-- WinUI 3 (does NOT work — use StaticResource) -->
|
||||
<Setter Property="Foreground" Value="{StaticResource TextColorBrush}"/>
|
||||
```
|
||||
|
||||
### Visual State Name Changes
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `MouseOver` | `PointerOver` |
|
||||
| `Disabled` | `Disabled` |
|
||||
| `Pressed` | `Pressed` |
|
||||
|
||||
## Resource Dictionary Changes
|
||||
|
||||
### Window.Resources → Grid.Resources
|
||||
|
||||
WinUI 3 `Window` is NOT a `DependencyObject` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
<Window>
|
||||
<Window.Resources>
|
||||
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
|
||||
</Window.Resources>
|
||||
<Grid>...</Grid>
|
||||
</Window>
|
||||
|
||||
<!-- WinUI 3 -->
|
||||
<Window>
|
||||
<Grid>
|
||||
<Grid.Resources>
|
||||
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
|
||||
</Grid.Resources>
|
||||
...
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### Theme Dictionaries
|
||||
|
||||
```xml
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="MyBrush" Color="#FF000000"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="MyBrush" Color="#FFFFFFFF"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="MyBrush" Color="{ThemeResource SystemColorWindowTextColor}"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
## URI Scheme Changes
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `pack://application:,,,/MyAssembly;component/image.png` | `ms-appx:///Assets/image.png` |
|
||||
| `pack://application:,,,/image.png` | `ms-appx:///image.png` |
|
||||
| Relative path `../image.png` | `ms-appx:///image.png` |
|
||||
|
||||
Assets directory convention: `Resources/` → `Assets/<Module>/`
|
||||
|
||||
## Data Binding Changes
|
||||
|
||||
### {Binding} vs {x:Bind}
|
||||
|
||||
Both are available. Prefer `{x:Bind}` for compile-time safety and performance.
|
||||
|
||||
| Feature | `{Binding}` | `{x:Bind}` |
|
||||
|---------|------------|------------|
|
||||
| Default mode | `OneWay` | **`OneTime`** (explicit `Mode=OneWay` required!) |
|
||||
| Context | `DataContext` | Code-behind class |
|
||||
| Resolution | Runtime | Compile-time |
|
||||
| Performance | Reflection-based | Compiled |
|
||||
| Function binding | No | Yes |
|
||||
|
||||
### WPF-Specific Binding Features to Remove
|
||||
|
||||
```xml
|
||||
<!-- These WPF-only features must be removed or rewritten -->
|
||||
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<!-- WinUI 3: UpdateSourceTrigger not needed; TextBox uses PropertyChanged by default -->
|
||||
<TextBox Text="{x:Bind ViewModel.Value, Mode=TwoWay}" />
|
||||
|
||||
{Binding RelativeSource={RelativeSource Self}, ...}
|
||||
<!-- WinUI 3: Use x:Bind which binds to the page itself, or use ElementName -->
|
||||
|
||||
<ItemsControl ItemsSource="{Binding}" />
|
||||
<!-- WinUI 3: Must specify explicit path -->
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.Items}" />
|
||||
```
|
||||
|
||||
## WPF-Only Window Properties to Remove
|
||||
|
||||
These properties exist on WPF `Window` but not WinUI 3:
|
||||
|
||||
```xml
|
||||
<!-- Remove from XAML — handle in code-behind via AppWindow API -->
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
ExtendsContentIntoTitleBar="True" <!-- Set in code-behind -->
|
||||
```
|
||||
|
||||
## XAML Control Property Changes
|
||||
|
||||
| WPF Property | WinUI 3 Property | Notes |
|
||||
|-------------|-----------------|-------|
|
||||
| `Focusable` | `IsTabStop` | Different name |
|
||||
| `SnapsToDevicePixels` | Not available | WinUI handles pixel snapping internally |
|
||||
| `UseLayoutRounding` | `UseLayoutRounding` | Same |
|
||||
| `IsHitTestVisible` | `IsHitTestVisible` | Same |
|
||||
| `TextBox.VerticalScrollBarVisibility` | `ScrollViewer.VerticalScrollBarVisibility` (attached) | Attached property |
|
||||
|
||||
## XAML Formatting (XamlStyler)
|
||||
|
||||
After migration, run XamlStyler to normalize formatting:
|
||||
- Alphabetize xmlns declarations and element attributes
|
||||
- Add UTF-8 BOM to all XAML files
|
||||
- Normalize comment spacing: `<!-- text -->` → `<!-- text -->`
|
||||
|
||||
PowerToys command: `.\.pipelines\applyXamlStyling.ps1 -Main`
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -360,3 +360,8 @@ src/common/Telemetry/*.etl
|
||||
# PowerToysInstaller Build Temp Files
|
||||
installer/*/*.wxs.bk
|
||||
/src/modules/awake/.claude
|
||||
|
||||
# Squad / Copilot agents — local-only, not committed
|
||||
.squad/
|
||||
.squad-workstream
|
||||
.github/agents/
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
"PowerToys.KeyboardManager.dll",
|
||||
|
||||
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
|
||||
"KeyboardManagerEditorUI\\PowerToys.KeyboardManagerEditorUI.exe",
|
||||
"WinUI3Apps\\PowerToys.KeyboardManagerEditorUI.exe",
|
||||
"WinUI3Apps\\PowerToys.KeyboardManagerEditorUI.dll",
|
||||
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
|
||||
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",
|
||||
"WinUI3Apps\\PowerToys.HostsModuleInterface.dll",
|
||||
|
||||
@@ -210,6 +210,9 @@ jobs:
|
||||
& '.pipelines/applyXamlStyling.ps1' -Passive
|
||||
displayName: Verify XAML formatting
|
||||
|
||||
- task: NuGetAuthenticate@1
|
||||
displayName: Authenticate NuGet feeds for verification
|
||||
|
||||
- pwsh: |-
|
||||
& '.pipelines/verifyNugetPackages.ps1' -solution '$(build.sourcesdirectory)\PowerToys.slnx'
|
||||
displayName: Verify Nuget package versions for PowerToys.slnx
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
@@ -40,7 +40,6 @@
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngine/KeyboardManagerEngine.vcxproj" Id="ba661f5b-1d5a-4ffc-9bf1-fc39df280bdd" />
|
||||
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManagerEngineLibrary.vcxproj" Id="e496b7fc-1e99-4bab-849b-0e8367040b02" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/keyboardmanager/MouseUtils/">
|
||||
<Folder Name="/modules/MouseUtils/">
|
||||
<Project Path="src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj" Id="48a1db8c-5df8-4fb3-9e14-2b67f3f2d8b5" />
|
||||
<Project Path="src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj" Id="e94fd11c-0591-456f-899f-efc0ca548336" />
|
||||
<Project Path="src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj" Id="782a61be-9d85-4081-b35c-1ccc9dcc1e88" />
|
||||
@@ -512,7 +512,7 @@
|
||||
</Project>
|
||||
<Project Path="src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj" Id="eae14c0e-7a6b-45da-9080-a7d8c077ba6e" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/keyboardmanager/MouseUtils/Tests/">
|
||||
<Folder Name="/modules/MouseUtils/Tests/">
|
||||
<Project Path="src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
256
README.md
256
README.md
@@ -51,19 +51,19 @@ But to get started quickly, choose one of the installation methods below:
|
||||
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysUserSetup-0.97.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysUserSetup-0.97.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysSetup-0.97.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysSetup-0.97.1-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.99%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysUserSetup-0.98.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysUserSetup-0.98.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysSetup-0.98.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysSetup-0.98.0-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.97.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.97.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.97.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.97.1-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.98.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.98.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.98.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.98.0-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -102,242 +102,14 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
## ✨ What's new?
|
||||
|
||||
**Version 0.97.2 (Feb 2026)**
|
||||
[](https://github.com/microsoft/PowerToys/releases)
|
||||
|
||||
This patch release fixes several important stability issues identified in v0.97.0 based on incoming reports. Check out the [v0.97.0](https://github.com/microsoft/PowerToys/releases/tag/v0.97.0) notes for the full list of changes.
|
||||
|
||||
## Advanced Paste
|
||||
- #45207 Fixed a crash in the Advanced Paste settings page caused by null values during JSON deserialization.
|
||||
|
||||
## Color Picker
|
||||
- #45367 Fixed contrast issue in Color picker UI.
|
||||
|
||||
## Command Palette
|
||||
- #45194 Fixed an issue where some Command Palette PowerToys Extension strings were not localised.
|
||||
|
||||
## Cursor Wrap
|
||||
- #45210 Fixed "Automatically activate on utility startup" setting not persisting when disabled. Thanks [@ThanhNguyxn](https://github.com/ThanhNguyxn)!
|
||||
- #45303 Added option to disable Cursor Wrapping when only a single monitor is connected. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
## Image Resizer
|
||||
- #45184 Fixed Image Resizer not working after upgrading PowerToys on Windows 10 by properly cleaning up legacy sparse app packages.
|
||||
|
||||
## LightSwitch
|
||||
- #45304 Fixed Light Switch startup logic to correctly apply the appropriate theme on launch.
|
||||
|
||||
## Workspaces
|
||||
- #45183 Fixed overlay positioning issue in workspace snapshot draw caused by DPI-aware coordinate mismatch.
|
||||
|
||||
## Quick Access and Measure Tool
|
||||
- #45443 Fixed crash related to `IsShownInSwitchers` property when Explorer is not running.
|
||||
|
||||
**Version 0.97.1 (January 2026)**
|
||||
|
||||
**Highlights**
|
||||
|
||||
### Advanced Paste
|
||||
- #44862: Fixed Settings UI advanced paste page crash by using correct settings repository for null checking.
|
||||
|
||||
### Command Palette
|
||||
- #44886: Fixed personalization section not appearing by using latest MSIX for installation.
|
||||
- #44938: Fixed loading of icons from internet shortcuts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- #45076: Fixed potential deadlock from lazy-loading AppListItem details. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Cursor Wrap
|
||||
- #44936: Added improved multi-monitor support; Added laptop lid close detection for dynamic monitor topology updates. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- #44936: Added new settings dropdown to constrain wrapping to horizontal-only, vertical-only, or both directions. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Peek
|
||||
- #44995: Fixed Space key triggering Peek during file rename, search, or address bar typing.
|
||||
|
||||
### PowerRename
|
||||
- #44944: Fixed regex `$` not working, preventing users from adding text at the end of filenames.
|
||||
|
||||
### Runner
|
||||
- #44931: Monochrome tray icon now adapts to Windows system theme instead of app theme.
|
||||
- #44982: Fixed right-click menu to dynamically update based on Quick Access enabled/disabled state.
|
||||
|
||||
### GPO / Enterprise
|
||||
- #45028: Added CursorWrap policy definition to ADMX templates. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
|
||||
For the full list of v0.97 changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
## Advanced Paste
|
||||
|
||||
- Added hex color previews in clipboard history. Thanks [@crramirez](https://github.com/crramirez)!
|
||||
- Added automatic placeholder endpoints when required fields are left empty.
|
||||
- Fixed a grammar issue in the AI settings description. Thanks [@erik-anderson](https://github.com/erik-anderson)!
|
||||
- Fixed loading order so custom action hotkeys are read correctly.
|
||||
- Updated Advanced Paste descriptions to reflect support for online and local models.
|
||||
- Fixed clipboard history item selection so it doesn’t duplicate entries.
|
||||
- Prevented placeholder endpoints from being saved for providers that don’t need them.
|
||||
- Added image input support for AI transforms and improved clipboard change tracking.
|
||||
|
||||
## Awake
|
||||
|
||||
- Fixed Awake CLI so help, errors, and logs appear correctly in the console. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
## Command Palette
|
||||
|
||||
- Fixed background image loading in BlurImageControl. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed SDK packaging paths and added a CI SDK build stage.
|
||||
- Aligned naming and spell-checking with .NET conventions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added drag-and-drop support for Command Palette items. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a PowerToys Command Palette extension to discover and launch PowerToys utilities.
|
||||
- Fixed grid view bindings and layout issues. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed a line-break issue in RDC extension toast messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made the Settings button text localizable. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Hid the RDC fallback on the home page and fixed MSTSC working directory handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Optimized result list merging for better performance. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added Small/Medium/Large detail sizes in the extensions API. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
- Hid fallback commands on the home page when no query is entered. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added back navigation support in the Settings window. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a Command Palette solution filter. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated Extension SDK documentation links to Microsoft Learn. Thanks [@RubenFricke](https://github.com/RubenFricke)!
|
||||
- Added a custom search engine URL setting for Web Search. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added pinyin matching for Chinese input. Thanks [@frg2089](https://github.com/frg2089)!
|
||||
- Bumped Command Palette version to 0.8.
|
||||
- Removed subtitles from built-in top-level commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Refined separator styling in the details pane. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a built-in Remote Desktop extension.
|
||||
- Added a Peek command to the Indexer extension.
|
||||
- Improved default browser detection using the Windows Shell API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Escape key behavior options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added theme and background customization options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved WinGet package app matching. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added an auto-return-home delay setting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added fallback ranking and global results settings.
|
||||
- Removed the selection indicator in the context menu list. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a developer ribbon with build and log info. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated the “Learn more” string for Command Palette. Thanks [@pratnala](https://github.com/pratnala)!
|
||||
- Added arrow-key navigation for grid views. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- Fixed version display when running unpackaged. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a native debugging launch profile. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Reduced redundant property change notifications in the SDK. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved section readability and accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made gallery spacing uniform. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added sections and separators for list and grid pages. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
|
||||
## Crop & Lock
|
||||
|
||||
- Added a screenshot mode that freezes a cropped region into its own window. Thanks [@fm-sys](https://github.com/fm-sys)!
|
||||
|
||||
## Cursor Wrap
|
||||
|
||||
- Improved Cursor Wrap behavior on multi-monitor setups by wrapping only at outer edges. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
## FancyZones
|
||||
|
||||
- Fixed editor overlay positioning on mixed-DPI multi-monitor setups. Thanks [@Memphizzz](https://github.com/Memphizzz)!
|
||||
- Added a FancyZones CLI for command-line layout management.
|
||||
|
||||
## File Locksmith
|
||||
|
||||
- Added a File Locksmith CLI for querying, waiting on, or killing file locks.
|
||||
|
||||
## Find My Mouse
|
||||
|
||||
- Improved spotlight edge rendering for clearer Find My Mouse visuals.
|
||||
- Added telemetry to track how Find My Mouse is triggered.
|
||||
|
||||
## Image Resizer
|
||||
|
||||
- Fixed Fill mode cropping when Shrink Only is enabled. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a dedicated Image Resizer CLI for scripted resizing.
|
||||
|
||||
## Light Switch
|
||||
|
||||
- Added telemetry events for Light Switch usage and settings changes.
|
||||
- Added a Follow Night Light mode to sync theme changes with Night Light.
|
||||
- Clarified LightSwitchService and LightSwitchStateManager roles in docs.
|
||||
- Added a Quick Access dashboard button to toggle Light Switch quickly.
|
||||
- Ensured Light Switch honors GPO policy states with clear status messaging.
|
||||
|
||||
## Mouse Without Borders
|
||||
|
||||
- Continued refactoring Mouse Without Borders by splitting the large Common class into focused components. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
- Completed the Common class refactor with Core and IPC helper extraction. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
## Peek
|
||||
|
||||
- Hardened Peek previews with strict resource filtering and safer external link warnings.
|
||||
- Improved SVG preview compatibility by rendering via WebView2.
|
||||
|
||||
## PowerRename
|
||||
|
||||
- Added HEIF/AVIF EXIF metadata extraction and extension status guidance for related previews.
|
||||
- Fixed undefined behavior in file time handling. Thanks [@safocl](https://github.com/safocl)!
|
||||
- Optimized memory allocation for depth-based rename processing.
|
||||
- Fixed Unicode normalization and non‑breaking space matching. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed date token replacements followed by capital letters. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
## PowerToys Run Plugins
|
||||
|
||||
- Fixed a plugin name typo and added Project Launcher to the third‑party list. Thanks [@artickc](https://github.com/artickc)!
|
||||
- Added the Open With Antigravity plugin to the third‑party list. Thanks [@artickc](https://github.com/artickc)!
|
||||
|
||||
## PowerToys Run
|
||||
|
||||
- Avoided unnecessary hotkey conflict checks when settings change.
|
||||
- Added QuickAI to the third-party PowerToys Run plugin list. Thanks [@ruslanlap](https://github.com/ruslanlap)!
|
||||
|
||||
## Quick Accent
|
||||
|
||||
- Added localized quotation marks to Quick Accent. Thanks [@warquys](https://github.com/warquys)!
|
||||
- Fixed duplicate and redundant characters in Quick Accent sets. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Fixed DPI positioning issues for Quick Accent on mixed-DPI setups. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
|
||||
## Settings
|
||||
|
||||
- Added a new tray icon that adapts to theme changes. Thanks [@HO-COOH](https://github.com/HO-COOH)!
|
||||
- Centralized module enable/disable logic for cleaner Settings UI updates.
|
||||
- Simplified Settings utilities by removing ISettingsUtils/ISettingsPath interfaces. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Improved Settings UI consistency and disabled-state visuals.
|
||||
- Added semantic headings to the Dashboard for better accessibility.
|
||||
- Introduced Quick Access as a standalone host with updated Settings integration.
|
||||
- Fixed Dashboard toggle flicker and sort menu checkmarks. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added Native AOT-compatible settings serialization.
|
||||
- Standardized mouse tool description text. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a global SettingsUtils singleton to reduce repeated initialization.
|
||||
|
||||
## Development
|
||||
|
||||
- Fixed broken devdocs links to the coding style guide. Thanks [@RubenFricke](https://github.com/RubenFricke)!
|
||||
- Migrated main and installer solutions to .slnx for improved build tooling.
|
||||
- Restored local installer builds after the WiX v5 upgrade with signing and versioning fixes.
|
||||
- Added incremental review tooling and structured AI prompts for PR/issue reviews.
|
||||
- Documented bot commands and cleaned up devdocs structure. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Updated WinAppSDK pipeline defaults to 1.8 and fixed restore handling.
|
||||
- Updated the COMMUNITY list to reflect current roles.
|
||||
- Maintained community member ordering and added a new entry.
|
||||
- Re-enabled centralized PackageReference for native projects with VS auto-restore.
|
||||
- Disabled MSBuild caching by default in CI to avoid build instability.
|
||||
- Updated the latest WinAppSDK daily pipeline for split-dependency restores.
|
||||
- Suppressed experimental build warnings and aligned WrapPanel stretch handling.
|
||||
- Reordered the spell-check expect list for consistent automation.
|
||||
- Migrated native projects to centralized PackageReference management.
|
||||
- Cleaned spell-check dictionary entries and capitalization.
|
||||
- Synced commit/PR prompts and wired VS Code to repo prompt files.
|
||||
- Added VS Code build tasks and improved build script path handling.
|
||||
- Updated Windows App SDK package versions in central package management.
|
||||
- Migrated cmdpal extension native project to PackageReference and fixed outputs.
|
||||
- Reverted PackageReference changes back to packages.config where needed.
|
||||
- Bypassed a release version check for a failing DLL to keep pipelines green.
|
||||
- Consolidated Copilot instructions and fixed prompt frontmatter.
|
||||
- Added signing entries for new Quick Access binaries and CLI version metadata.
|
||||
- Fixed install scope detection to avoid mixed per-user/per-machine installs.
|
||||
- Added a Module Loader tool to quickly test PowerToys modules without full builds. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added update telemetry to understand auto-update checks and downloads.
|
||||
- Updated the telemetry package for new compliance requirements. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
|
||||
- Documented missing telemetry events in DATA_AND_PRIVACY.
|
||||
- Fixed UI test pipeline restores for .slnx solutions.
|
||||
- Added UI automation coverage for Advanced Paste clipboard history flows.
|
||||
- Stabilized FancyZones UI tests with more reliable selectors and screen recordings.
|
||||
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.0).
|
||||
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.98][github-next-release-work]!
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.99][github-next-release-work]!
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
|
||||
|
||||
BIN
doc/images/readme/Release-Banner.png
Normal file
BIN
doc/images/readme/Release-Banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
@@ -22,6 +22,16 @@
|
||||
|
||||
<ComponentGroup Id="DscResourcesComponentGroup">
|
||||
<ComponentRef Id="PowerToysDSCReference" />
|
||||
<?if $(var.PerUser) = "false" ?>
|
||||
<Component Id="SecureDSCModulesFolder" Guid="7D2F4E57-CCB2-4F89-9B8B-62E9B3CC4E12" Directory="DSCModulesReferenceFolder" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="SecureDSCModulesFolder" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<CreateFolder>
|
||||
<PermissionEx Sddl="D:PAI(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)(A;OICI;GRGX;;;BU)(A;OICIIO;GA;;;CO)" />
|
||||
</CreateFolder>
|
||||
</Component>
|
||||
<?endif?>
|
||||
<Component Id="RemoveDSCModulesFolder" Guid="A3C77D92-4E97-4C1A-9F2E-8B3C5D6E7F80" Directory="DSCModulesReferenceFolder">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemoveDSCModulesFolder" Value="" KeyPath="yes" />
|
||||
|
||||
@@ -2,7 +2,29 @@
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define KeyboardManagerAssetsFiles=?>
|
||||
<?define KeyboardManagerAssetsWinUI3Files=?>
|
||||
<?define KeyboardManagerAssetsFilesPath=$(var.BinDir)\Assets\KeyboardManager\?>
|
||||
<?define KeyboardManagerAssetsWinUI3FilesPath=$(var.BinDir)\WinUI3Apps\Assets\KeyboardManagerEditor\?>
|
||||
|
||||
<Fragment>
|
||||
<DirectoryRef Id="BaseApplicationsAssetsFolder">
|
||||
<Directory Id="KeyboardManagerAssetsInstallFolder" Name="KeyboardManager" />
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="KeyboardManagerAssetsWinUI3InstallFolder" Name="KeyboardManagerEditor" />
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="KeyboardManagerAssetsInstallFolder" FileSource="$(var.KeyboardManagerAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--KeyboardManagerAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="KeyboardManagerAssetsWinUI3InstallFolder" FileSource="$(var.KeyboardManagerAssetsWinUI3FilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--KeyboardManagerAssetsWinUI3Files_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Directory Id="KeyboardManagerEditorInstallFolder" Name="KeyboardManagerEditor" />
|
||||
<Directory Id="KeyboardManagerEngineInstallFolder" Name="KeyboardManagerEngine" />
|
||||
@@ -44,6 +66,8 @@
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemoveKeyboardManagerFolder" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderKeyboardManagerAssetsInstallFolder" Directory="KeyboardManagerAssetsInstallFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveFolderKeyboardManagerAssetsWinUI3InstallFolder" Directory="KeyboardManagerAssetsWinUI3InstallFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveFolderKeyboardManagerEditorFolder" Directory="KeyboardManagerEditorInstallFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveFolderKeyboardManagerEngineFolder" Directory="KeyboardManagerEngineInstallFolder" On="uninstall" />
|
||||
</Component>
|
||||
|
||||
@@ -172,6 +172,12 @@ Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptR
|
||||
Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer"
|
||||
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs
|
||||
|
||||
#KeyboardManager
|
||||
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsFiles -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\KeyboardManager"
|
||||
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsWinUI3Files -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\KeyboardManagerEditor"
|
||||
Generate-FileComponents -fileListName "KeyboardManagerAssetsFiles" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs
|
||||
Generate-FileComponents -fileListName "KeyboardManagerAssetsWinUI3Files" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs
|
||||
|
||||
# Light Switch Service
|
||||
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
|
||||
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h actionRunner.base.rc actionRunner.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h actionRunner.base.rc actionRunner.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h PowerToys.Update.base.rc PowerToys.Update.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h PowerToys.Update.base.rc PowerToys.Update.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
|
||||
@@ -287,8 +287,26 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE;
|
||||
}
|
||||
hstring Constants::MWBToggleEasyMouseEvent()
|
||||
{
|
||||
return CommonSharedConstants::MWB_TOGGLE_EASY_MOUSE_EVENT;
|
||||
}
|
||||
hstring Constants::MWBReconnectEvent()
|
||||
{
|
||||
return CommonSharedConstants::MWB_RECONNECT_EVENT;
|
||||
}
|
||||
|
||||
hstring Constants::OpenNewKeyboardManagerEvent()
|
||||
{
|
||||
return CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT;
|
||||
}
|
||||
hstring Constants::ToggleKeyboardManagerActiveEvent()
|
||||
{
|
||||
return CommonSharedConstants::TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT;
|
||||
}
|
||||
hstring Constants::KeyboardManagerEngineInstanceMutex()
|
||||
{
|
||||
return CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,11 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring PowerDisplayToggleMessage();
|
||||
static hstring PowerDisplayApplyProfileMessage();
|
||||
static hstring PowerDisplayTerminateAppMessage();
|
||||
static hstring MWBToggleEasyMouseEvent();
|
||||
static hstring MWBReconnectEvent();
|
||||
static hstring OpenNewKeyboardManagerEvent();
|
||||
static hstring ToggleKeyboardManagerActiveEvent();
|
||||
static hstring KeyboardManagerEngineInstanceMutex();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,3 +89,4 @@ namespace winrt::PowerToys::Interop::factory_implementation
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,12 @@ namespace PowerToys
|
||||
static String PowerDisplayToggleMessage();
|
||||
static String PowerDisplayApplyProfileMessage();
|
||||
static String PowerDisplayTerminateAppMessage();
|
||||
static String MWBToggleEasyMouseEvent();
|
||||
static String MWBReconnectEvent();
|
||||
static String OpenNewKeyboardManagerEvent();
|
||||
static String ToggleKeyboardManagerActiveEvent();
|
||||
static String KeyboardManagerEngineInstanceMutex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -172,11 +172,18 @@ namespace CommonSharedConstants
|
||||
|
||||
// Path to events used by Keyboard Manager
|
||||
const wchar_t OPEN_NEW_KEYBOARD_MANAGER_EVENT[] = L"Local\\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f";
|
||||
const wchar_t TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT[] = L"Local\\PowerToysToggleKeyboardManagerActiveEvent-7f3a1d5c-2e94-4ff4-8b6a-90fd2bc4d2a7";
|
||||
const wchar_t KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX[] = L"Local\\PowerToys_KBMEngine_InstanceMutex";
|
||||
|
||||
// used from quick access window
|
||||
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
||||
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";
|
||||
|
||||
// Path to the events used by MouseWithoutBorders
|
||||
const wchar_t MWB_TOGGLE_EASY_MOUSE_EVENT[] = L"Local\\PowerToysMWB-ToggleEasyMouseEvent-a9c8d7b6-e5f4-3c2a-1b0d-9e8f7a6b5c4d";
|
||||
const wchar_t MWB_RECONNECT_EVENT[] = L"Local\\PowerToysMWB-ReconnectEvent-b8d7c6a5-f4e3-2b1c-0a9d-8e7f6a5b4c3d";
|
||||
|
||||
// Max DWORD for key code to disable keys.
|
||||
const DWORD VK_DISABLED = 0x100;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '..\..\..\..\tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h AdvancedPaste.base.rc AdvancedPaste.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted ..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h AdvancedPaste.base.rc AdvancedPaste.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>15.0</VCProjectVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h EnvironmentVariablesModuleInterface.base.rc EnvironmentVariablesModuleInterface.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h EnvironmentVariablesModuleInterface.base.rc EnvironmentVariablesModuleInterface.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h FileLocksmithContextMenu.base.rc FileLocksmithContextMenu.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h FileLocksmithContextMenu.base.rc FileLocksmithContextMenu.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h FileLocksmithExt.base.rc FileLocksmithExt.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h FileLocksmithExt.base.rc FileLocksmithExt.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '..\..\..\..\tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h HostsModuleInterface.base.rc HostsModuleInterface.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted ..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h HostsModuleInterface.base.rc HostsModuleInterface.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
|
||||
@@ -16,7 +16,6 @@ using System.Threading.Tasks;
|
||||
using HostsUILib.Exceptions;
|
||||
using HostsUILib.Models;
|
||||
using HostsUILib.Settings;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
@@ -223,64 +222,17 @@ namespace HostsUILib.Helpers
|
||||
|
||||
public void OpenHostsFile()
|
||||
{
|
||||
var notepadFallback = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Try to open in default editor
|
||||
var key = Registry.ClassesRoot.OpenSubKey("SystemFileAssociations\\text\\shell\\edit\\command");
|
||||
if (key != null)
|
||||
{
|
||||
var commandPattern = key.GetValue(string.Empty).ToString(); // Default value
|
||||
var file = null as string;
|
||||
var args = null as string;
|
||||
|
||||
if (commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
|
||||
if (endQuoteIndex != -1)
|
||||
{
|
||||
file = commandPattern[1..endQuoteIndex];
|
||||
args = commandPattern[(endQuoteIndex + 1)..].Trim();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spaceIndex = commandPattern.IndexOf(' ');
|
||||
if (spaceIndex != -1)
|
||||
{
|
||||
file = commandPattern[..spaceIndex];
|
||||
args = commandPattern[(spaceIndex + 1)..].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (file != null && args != null)
|
||||
{
|
||||
args = args.Replace("%1", HostsFilePath);
|
||||
Process.Start(new ProcessStartInfo(file, args));
|
||||
}
|
||||
else
|
||||
{
|
||||
notepadFallback = true;
|
||||
}
|
||||
}
|
||||
var notepadPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Windows),
|
||||
"System32",
|
||||
"notepad.exe");
|
||||
Process.Start(new ProcessStartInfo(notepadPath, HostsFilePath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to open default editor", ex);
|
||||
notepadFallback = true;
|
||||
}
|
||||
|
||||
if (notepadFallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("notepad.exe", HostsFilePath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to open notepad", ex);
|
||||
}
|
||||
LoggerInstance.Logger.LogError("Failed to open notepad", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
|
||||
Logger::info(L"======= TOPOLOGY INITIALIZATION START =======");
|
||||
Logger::info(L"Initializing edge-based topology for {} monitors", monitors.size());
|
||||
|
||||
|
||||
m_monitors = monitors;
|
||||
m_outerEdges.clear();
|
||||
m_edgeMap.clear();
|
||||
@@ -692,7 +691,6 @@ int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativ
|
||||
return static_cast<int>(result);
|
||||
}
|
||||
|
||||
|
||||
std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const
|
||||
{
|
||||
std::vector<GapInfo> gaps;
|
||||
|
||||
@@ -84,7 +84,7 @@ private:
|
||||
bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag
|
||||
bool m_disableOnSingleMonitor = false; // Default to false
|
||||
int m_wrapMode = 0; // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
|
||||
int m_activationMode = 0; // 0=Always (default), 1=HoldingCtrl (disables wrap), 2=HoldingShift (disables wrap)
|
||||
int m_activationMode = 0; // 0=Always (default), 1=HoldingCtrl (wraps only while held), 2=HoldingShift (wraps only while held)
|
||||
|
||||
// Mouse hook
|
||||
HHOOK m_mouseHook = nullptr;
|
||||
@@ -689,23 +689,23 @@ private:
|
||||
|
||||
if (g_cursorWrapInstance && g_cursorWrapInstance->m_hookActive)
|
||||
{
|
||||
// Check activation mode to determine if wrapping should be disabled
|
||||
// 0=Always, 1=HoldingCtrl (disables wrap when Ctrl held), 2=HoldingShift (disables wrap when Shift held)
|
||||
// Check activation mode to determine if wrapping should happen.
|
||||
// 0=Always, 1=HoldingCtrl (wraps only when Ctrl held), 2=HoldingShift (wraps only when Shift held)
|
||||
int activationMode = g_cursorWrapInstance->m_activationMode;
|
||||
bool disableByKey = false;
|
||||
bool shouldWrap = true;
|
||||
|
||||
if (activationMode == 1) // HoldingCtrl - disable wrap when Ctrl is held
|
||||
if (activationMode == 1) // HoldingCtrl - wrap only when Ctrl is held
|
||||
{
|
||||
disableByKey = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
shouldWrap = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
}
|
||||
else if (activationMode == 2) // HoldingShift - disable wrap when Shift is held
|
||||
else if (activationMode == 2) // HoldingShift - wrap only when Shift is held
|
||||
{
|
||||
disableByKey = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||
shouldWrap = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
if (disableByKey)
|
||||
if (!shouldWrap)
|
||||
{
|
||||
// Key is held, do not wrap - let normal behavior happen
|
||||
// Activation key is not held, do not wrap - let normal behavior happen.
|
||||
return CallNextHookEx(nullptr, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ namespace MouseWithoutBorders.Class
|
||||
if (!Common.RunOnLogonDesktop)
|
||||
{
|
||||
StartSettingSyncThread();
|
||||
CommandEventHandler.StartListening();
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
|
||||
114
src/modules/MouseWithoutBorders/App/Core/CommandEventHandler.cs
Normal file
114
src/modules/MouseWithoutBorders/App/Core/CommandEventHandler.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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.Threading;
|
||||
|
||||
using MouseWithoutBorders.Class;
|
||||
using PowerToys.Interop;
|
||||
|
||||
namespace MouseWithoutBorders.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles command events from external sources (e.g., Command Palette).
|
||||
/// Uses named events for inter-process communication, following the same pattern as other PowerToys modules.
|
||||
/// </summary>
|
||||
internal static class CommandEventHandler
|
||||
{
|
||||
private static CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// Starts listening for command events on background threads.
|
||||
/// </summary>
|
||||
public static void StartListening()
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
CancellationToken exitToken = _cancellationTokenSource.Token;
|
||||
|
||||
// Start listener for Toggle Easy Mouse event
|
||||
StartEventListener(Constants.MWBToggleEasyMouseEvent(), ToggleEasyMouse, exitToken);
|
||||
|
||||
// Start listener for Reconnect event
|
||||
StartEventListener(Constants.MWBReconnectEvent(), Reconnect, exitToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops listening for command events.
|
||||
/// </summary>
|
||||
public static void StopListening()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
|
||||
private static void StartEventListener(string eventName, Action callback, CancellationToken cancel)
|
||||
{
|
||||
new System.Threading.Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
|
||||
WaitHandle[] waitHandles = new WaitHandle[] { cancel.WaitHandle, eventHandle };
|
||||
|
||||
while (!cancel.IsCancellationRequested)
|
||||
{
|
||||
int result = WaitHandle.WaitAny(waitHandles);
|
||||
if (result == 1)
|
||||
{
|
||||
// Execute callback on UI thread using Common.DoSomethingInUIThread
|
||||
Common.DoSomethingInUIThread(callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cancellation requested
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Error in event listener for {eventName}: {ex.Message}");
|
||||
}
|
||||
})
|
||||
{ IsBackground = true, Name = $"MWB-{eventName}-Listener" }.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles Easy Mouse between Enabled and Disabled states.
|
||||
/// This is the same logic used by the hotkey handler.
|
||||
/// </summary>
|
||||
public static void ToggleEasyMouse()
|
||||
{
|
||||
if (Common.RunOnLogonDesktop || Common.RunOnScrSaverDesktop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EasyMouseOption easyMouseOption = (EasyMouseOption)Setting.Values.EasyMouse;
|
||||
|
||||
if (easyMouseOption is EasyMouseOption.Disable or EasyMouseOption.Enable)
|
||||
{
|
||||
Setting.Values.EasyMouse = (int)(easyMouseOption == EasyMouseOption.Disable ? EasyMouseOption.Enable : EasyMouseOption.Disable);
|
||||
|
||||
Common.ShowToolTip($"Easy Mouse has been toggled to [{(EasyMouseOption)Setting.Values.EasyMouse}].", 3000);
|
||||
|
||||
Logger.Log($"Easy Mouse toggled to {(EasyMouseOption)Setting.Values.EasyMouse} via command event.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a reconnection attempt to all machines.
|
||||
/// This is the same logic used by the hotkey handler.
|
||||
/// </summary>
|
||||
public static void Reconnect()
|
||||
{
|
||||
Common.ShowToolTip("Reconnecting...", 2000);
|
||||
Common.LastReconnectByHotKeyTime = Common.GetTick();
|
||||
InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_HOTKEY;
|
||||
|
||||
Logger.Log("Reconnect initiated via command event.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,6 +218,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h new.base.rc new.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h new.base.rc new.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h new.base.rc new.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h new.base.rc new.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h PowerOCR.base.rc PowerOCR.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h PowerOCR.base.rc PowerOCR.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>15.0</VCProjectVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h ShortcutGuide.base.rc ShortcutGuide.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ShortcutGuide.base.rc ShortcutGuide.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<CppWinRTOptimized>true</CppWinRTOptimized>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h ShortcutGuideModuleInterface.base.rc ShortcutGuideModuleInterface.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ShortcutGuideModuleInterface.base.rc ShortcutGuideModuleInterface.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>15.0</VCProjectVersion>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- Props that should be disabled while building on CI server -->
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(SolutionDir)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h WorkspacesLauncherResource.base.rc WorkspacesLauncherResource.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h WorkspacesLauncherResource.base.rc WorkspacesLauncherResource.rc" />
|
||||
</Target>
|
||||
<!-- C++ source compile-specific things for all configurations -->
|
||||
<ItemDefinitionGroup>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- Props that should be disabled while building on CI server -->
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(SolutionDir)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h WorkspacesSnapshotToolResources.base.rc WorkspacesSnapshotToolResources.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h WorkspacesSnapshotToolResources.base.rc WorkspacesSnapshotToolResources.rc" />
|
||||
</Target>
|
||||
<!-- C++ source compile-specific things for all configurations -->
|
||||
<ItemDefinitionGroup>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- Props that should be disabled while building on CI server -->
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(SolutionDir)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h WorkspacesWindowArrangerResource.base.rc WorkspacesWindowArrangerResource.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h WorkspacesWindowArrangerResource.base.rc WorkspacesWindowArrangerResource.rc" />
|
||||
</Target>
|
||||
<!-- C++ source compile-specific things for all configurations -->
|
||||
<ItemDefinitionGroup>
|
||||
|
||||
@@ -5679,6 +5679,16 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
// Recording completed (closed via hotkey or item close). Proceed to save/trim workflow.
|
||||
OutputDebugStringW(L"[Recording] StartAsync completed, entering save workflow\n");
|
||||
|
||||
// Release the writer stream and session objects before trim/save. Keeping the temp file
|
||||
// open here can cause trimming and later MoveAndReplaceAsync calls to fail on the same file.
|
||||
if (stream)
|
||||
{
|
||||
stream.Close();
|
||||
stream = nullptr;
|
||||
}
|
||||
g_RecordingSession = nullptr;
|
||||
g_GifRecordingSession = nullptr;
|
||||
|
||||
// Resume on the UI thread for the save dialog
|
||||
co_await uiThread;
|
||||
OutputDebugStringW(L"[Recording] Resumed on UI thread\n");
|
||||
|
||||
@@ -71,7 +71,8 @@ bool isExcluded(HWND window)
|
||||
auto processPath = get_process_path(window);
|
||||
CharUpperBuffW(processPath.data(), static_cast<DWORD>(processPath.length()));
|
||||
|
||||
return check_excluded_app(window, processPath, AlwaysOnTopSettings::settings().excludedApps);
|
||||
const auto settings = AlwaysOnTopSettings::settings();
|
||||
return check_excluded_app(window, processPath, settings->excludedApps);
|
||||
}
|
||||
|
||||
AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
|
||||
@@ -155,7 +156,8 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
|
||||
break;
|
||||
case SettingId::FrameEnabled:
|
||||
{
|
||||
if (AlwaysOnTopSettings::settings().enableFrame)
|
||||
const auto settings = AlwaysOnTopSettings::settings();
|
||||
if (settings->enableFrame)
|
||||
{
|
||||
for (auto& iter : m_topmostWindows)
|
||||
{
|
||||
@@ -194,7 +196,8 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
|
||||
break;
|
||||
case SettingId::ShowInSystemMenu:
|
||||
{
|
||||
UpdateSystemMenuEventHooks(AlwaysOnTopSettings::settings().showInSystemMenu);
|
||||
const auto settings = AlwaysOnTopSettings::settings();
|
||||
UpdateSystemMenuEventHooks(settings->showInSystemMenu);
|
||||
m_lastSystemMenuWindow = nullptr;
|
||||
UpdateSystemMenuItem(GetForegroundWindow());
|
||||
}
|
||||
@@ -236,7 +239,7 @@ LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lp
|
||||
void AlwaysOnTop::ProcessCommand(HWND window)
|
||||
{
|
||||
bool gameMode = detect_game_mode();
|
||||
if (AlwaysOnTopSettings::settings().blockInGameMode && gameMode)
|
||||
if (AlwaysOnTopSettings::settings()->blockInGameMode && gameMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -276,7 +279,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
|
||||
}
|
||||
}
|
||||
|
||||
if (AlwaysOnTopSettings::settings().enableSound)
|
||||
if (AlwaysOnTopSettings::settings()->enableSound)
|
||||
{
|
||||
m_sound.Play(soundType);
|
||||
}
|
||||
@@ -323,7 +326,7 @@ void AlwaysOnTop::StartTrackingTopmostWindows()
|
||||
|
||||
bool AlwaysOnTop::AssignBorder(HWND window)
|
||||
{
|
||||
if (m_virtualDesktopUtils.IsWindowOnCurrentDesktop(window) && AlwaysOnTopSettings::settings().enableFrame)
|
||||
if (m_virtualDesktopUtils.IsWindowOnCurrentDesktop(window) && AlwaysOnTopSettings::settings()->enableFrame)
|
||||
{
|
||||
auto border = WindowBorder::Create(window, m_hinstance);
|
||||
if (border)
|
||||
@@ -352,11 +355,13 @@ void AlwaysOnTop::RegisterHotkey() const
|
||||
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity));
|
||||
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity));
|
||||
|
||||
const auto settings = AlwaysOnTopSettings::settings();
|
||||
|
||||
// Register pin hotkey
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code());
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), settings->hotkey.get_modifiers(), settings->hotkey.get_code());
|
||||
|
||||
// Register transparency hotkeys using the same modifiers as the pin hotkey
|
||||
UINT modifiers = AlwaysOnTopSettings::settings().hotkey.get_modifiers();
|
||||
UINT modifiers = settings->hotkey.get_modifiers();
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
|
||||
}
|
||||
@@ -472,7 +477,7 @@ void AlwaysOnTop::SubscribeToEvents()
|
||||
}
|
||||
}
|
||||
|
||||
UpdateSystemMenuEventHooks(AlwaysOnTopSettings::settings().showInSystemMenu);
|
||||
UpdateSystemMenuEventHooks(AlwaysOnTopSettings::settings()->showInSystemMenu);
|
||||
}
|
||||
|
||||
void AlwaysOnTop::UpdateSystemMenuEventHooks(bool enable)
|
||||
@@ -525,7 +530,8 @@ void AlwaysOnTop::UpdateSystemMenuItem(HWND window) const noexcept
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AlwaysOnTopSettings::settings().showInSystemMenu)
|
||||
const auto settings = AlwaysOnTopSettings::settings();
|
||||
if (!settings->showInSystemMenu)
|
||||
{
|
||||
if (IsAlwaysOnTopMenuCommand(systemMenu))
|
||||
{
|
||||
@@ -644,7 +650,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
|
||||
{
|
||||
if (data->idObject == OBJID_SYSMENU && data->hwnd)
|
||||
{
|
||||
m_lastSystemMenuWindow = AlwaysOnTopSettings::settings().showInSystemMenu ? data->hwnd : nullptr;
|
||||
m_lastSystemMenuWindow = AlwaysOnTopSettings::settings()->showInSystemMenu ? data->hwnd : nullptr;
|
||||
UpdateSystemMenuItem(data->hwnd);
|
||||
}
|
||||
}
|
||||
@@ -659,7 +665,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
|
||||
return;
|
||||
case EVENT_OBJECT_INVOKED:
|
||||
{
|
||||
if (!AlwaysOnTopSettings::settings().showInSystemMenu)
|
||||
if (!AlwaysOnTopSettings::settings()->showInSystemMenu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -710,7 +716,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
|
||||
break;
|
||||
}
|
||||
|
||||
if (!AlwaysOnTopSettings::settings().enableFrame || !data->hwnd)
|
||||
if (!AlwaysOnTopSettings::settings()->enableFrame || !data->hwnd)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -879,7 +885,7 @@ void AlwaysOnTop::StepWindowTransparency(HWND window, int delta)
|
||||
{
|
||||
ApplyWindowAlpha(targetWindow, newTransparency);
|
||||
|
||||
if (AlwaysOnTopSettings::settings().enableSound)
|
||||
if (AlwaysOnTopSettings::settings()->enableSound)
|
||||
{
|
||||
m_sound.Play(delta > 0 ? Sound::Type::IncreaseOpacity : Sound::Type::DecreaseOpacity);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- Props that should be disabled while building on CI server -->
|
||||
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted '$(RepoRoot)tools\build\convert-resx-to-rc.ps1' '$(MSBuildThisFileDirectory)' resource.base.h resource.h AlwaysOnTop.base.rc AlwaysOnTop.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h AlwaysOnTop.base.rc AlwaysOnTop.rc" />
|
||||
</Target>
|
||||
<!-- C++ source compile-specific things for all configurations -->
|
||||
<ItemDefinitionGroup>
|
||||
|
||||
@@ -44,12 +44,14 @@ inline COLORREF HexToRGB(std::wstring_view hex, const COLORREF fallbackColor = R
|
||||
}
|
||||
}
|
||||
|
||||
AlwaysOnTopSettings::AlwaysOnTopSettings()
|
||||
AlwaysOnTopSettings::AlwaysOnTopSettings() :
|
||||
m_settings(std::make_shared<Settings>())
|
||||
{
|
||||
m_uiSettings.ColorValuesChanged([&](winrt::Windows::UI::ViewManagement::UISettings const& settings,
|
||||
winrt::Windows::Foundation::IInspectable const& args)
|
||||
{
|
||||
if (m_settings.frameAccentColor)
|
||||
const auto currentSettings = AlwaysOnTopSettings::settings();
|
||||
if (currentSettings->frameAccentColor)
|
||||
{
|
||||
NotifyObservers(SettingId::FrameAccentColor);
|
||||
}
|
||||
@@ -95,94 +97,97 @@ void AlwaysOnTopSettings::LoadSettings()
|
||||
try
|
||||
{
|
||||
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(NonLocalizable::ModuleKey);
|
||||
const auto currentSettings = AlwaysOnTopSettings::settings();
|
||||
auto updatedSettings = std::make_shared<Settings>(*currentSettings);
|
||||
std::vector<SettingId> changedSettings;
|
||||
|
||||
if (const auto jsonVal = values.get_json(NonLocalizable::HotkeyID))
|
||||
{
|
||||
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
|
||||
if (m_settings.hotkey.get_modifiers() != val.get_modifiers() || m_settings.hotkey.get_key() != val.get_key() || m_settings.hotkey.get_code() != val.get_code())
|
||||
if (updatedSettings->hotkey.get_modifiers() != val.get_modifiers() || updatedSettings->hotkey.get_key() != val.get_key() || updatedSettings->hotkey.get_code() != val.get_code())
|
||||
{
|
||||
m_settings.hotkey = val;
|
||||
NotifyObservers(SettingId::Hotkey);
|
||||
updatedSettings->hotkey = val;
|
||||
changedSettings.push_back(SettingId::Hotkey);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::SoundEnabledID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.enableSound != val)
|
||||
if (updatedSettings->enableSound != val)
|
||||
{
|
||||
m_settings.enableSound = val;
|
||||
NotifyObservers(SettingId::SoundEnabled);
|
||||
updatedSettings->enableSound = val;
|
||||
changedSettings.push_back(SettingId::SoundEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::ShowInSystemMenuID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.showInSystemMenu != val)
|
||||
if (updatedSettings->showInSystemMenu != val)
|
||||
{
|
||||
m_settings.showInSystemMenu = val;
|
||||
NotifyObservers(SettingId::ShowInSystemMenu);
|
||||
updatedSettings->showInSystemMenu = val;
|
||||
changedSettings.push_back(SettingId::ShowInSystemMenu);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_int_value(NonLocalizable::FrameThicknessID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.frameThickness != val)
|
||||
if (updatedSettings->frameThickness != val)
|
||||
{
|
||||
m_settings.frameThickness = val;
|
||||
NotifyObservers(SettingId::FrameThickness);
|
||||
updatedSettings->frameThickness = val;
|
||||
changedSettings.push_back(SettingId::FrameThickness);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_string_value(NonLocalizable::FrameColorID))
|
||||
{
|
||||
auto val = HexToRGB(*jsonVal);
|
||||
if (m_settings.frameColor != val)
|
||||
if (updatedSettings->frameColor != val)
|
||||
{
|
||||
m_settings.frameColor = val;
|
||||
NotifyObservers(SettingId::FrameColor);
|
||||
updatedSettings->frameColor = val;
|
||||
changedSettings.push_back(SettingId::FrameColor);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_int_value(NonLocalizable::FrameOpacityID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.frameOpacity != val)
|
||||
if (updatedSettings->frameOpacity != val)
|
||||
{
|
||||
m_settings.frameOpacity = val;
|
||||
NotifyObservers(SettingId::FrameOpacity);
|
||||
updatedSettings->frameOpacity = val;
|
||||
changedSettings.push_back(SettingId::FrameOpacity);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::FrameEnabledID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.enableFrame != val)
|
||||
if (updatedSettings->enableFrame != val)
|
||||
{
|
||||
m_settings.enableFrame = val;
|
||||
NotifyObservers(SettingId::FrameEnabled);
|
||||
updatedSettings->enableFrame = val;
|
||||
changedSettings.push_back(SettingId::FrameEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::BlockInGameModeID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.blockInGameMode != val)
|
||||
if (updatedSettings->blockInGameMode != val)
|
||||
{
|
||||
m_settings.blockInGameMode = val;
|
||||
NotifyObservers(SettingId::BlockInGameMode);
|
||||
updatedSettings->blockInGameMode = val;
|
||||
changedSettings.push_back(SettingId::BlockInGameMode);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::RoundCornersEnabledID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.roundCornersEnabled != val)
|
||||
if (updatedSettings->roundCornersEnabled != val)
|
||||
{
|
||||
m_settings.roundCornersEnabled = val;
|
||||
NotifyObservers(SettingId::RoundCornersEnabled);
|
||||
updatedSettings->roundCornersEnabled = val;
|
||||
changedSettings.push_back(SettingId::RoundCornersEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,20 +208,29 @@ void AlwaysOnTopSettings::LoadSettings()
|
||||
view = left_trim<wchar_t>(trim<wchar_t>(view));
|
||||
}
|
||||
|
||||
if (m_settings.excludedApps != excludedApps)
|
||||
if (updatedSettings->excludedApps != excludedApps)
|
||||
{
|
||||
m_settings.excludedApps = excludedApps;
|
||||
NotifyObservers(SettingId::ExcludeApps);
|
||||
updatedSettings->excludedApps = excludedApps;
|
||||
changedSettings.push_back(SettingId::ExcludeApps);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::FrameAccentColor))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.frameAccentColor != val)
|
||||
if (updatedSettings->frameAccentColor != val)
|
||||
{
|
||||
m_settings.frameAccentColor = val;
|
||||
NotifyObservers(SettingId::FrameAccentColor);
|
||||
updatedSettings->frameAccentColor = val;
|
||||
changedSettings.push_back(SettingId::FrameAccentColor);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changedSettings.empty())
|
||||
{
|
||||
m_settings.store(std::shared_ptr<const Settings>(updatedSettings), std::memory_order_release);
|
||||
for (const auto changedSetting : changedSettings)
|
||||
{
|
||||
NotifyObservers(changedSetting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <common/SettingsAPI/FileWatcher.h>
|
||||
#include <common/SettingsAPI/settings_objects.h>
|
||||
@@ -34,9 +37,9 @@ class AlwaysOnTopSettings
|
||||
{
|
||||
public:
|
||||
static AlwaysOnTopSettings& instance();
|
||||
static inline const Settings& settings()
|
||||
static inline std::shared_ptr<const Settings> settings()
|
||||
{
|
||||
return instance().m_settings;
|
||||
return instance().m_settings.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void InitFileWatcher();
|
||||
@@ -52,7 +55,7 @@ private:
|
||||
~AlwaysOnTopSettings() = default;
|
||||
|
||||
winrt::Windows::UI::ViewManagement::UISettings m_uiSettings;
|
||||
Settings m_settings;
|
||||
std::atomic<std::shared_ptr<const Settings>> m_settings;
|
||||
std::unique_ptr<FileWatcher> m_settingsFileWatcher;
|
||||
std::unordered_set<SettingsObserver*> m_observers;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ std::optional<RECT> GetFrameRect(HWND window)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int border = AlwaysOnTopSettings::settings().frameThickness;
|
||||
int border = AlwaysOnTopSettings::settings()->frameThickness;
|
||||
rect.top -= border;
|
||||
rect.left -= border;
|
||||
rect.right += border;
|
||||
@@ -194,8 +194,9 @@ void WindowBorder::UpdateBorderProperties() const
|
||||
|
||||
RECT frameRect{ 0, 0, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
|
||||
|
||||
const auto settings = AlwaysOnTopSettings::settings();
|
||||
COLORREF color;
|
||||
if (AlwaysOnTopSettings::settings().frameAccentColor)
|
||||
if (settings->frameAccentColor)
|
||||
{
|
||||
winrt::Windows::UI::ViewManagement::UISettings settings;
|
||||
auto accentValue = settings.GetColorValue(winrt::Windows::UI::ViewManagement::UIColorType::Accent);
|
||||
@@ -203,14 +204,14 @@ void WindowBorder::UpdateBorderProperties() const
|
||||
}
|
||||
else
|
||||
{
|
||||
color = AlwaysOnTopSettings::settings().frameColor;
|
||||
color = settings->frameColor;
|
||||
}
|
||||
|
||||
float opacity = AlwaysOnTopSettings::settings().frameOpacity / 100.0f;
|
||||
float opacity = settings->frameOpacity / 100.0f;
|
||||
float scalingFactor = ScalingUtils::ScalingFactor(m_trackingWindow);
|
||||
float thickness = AlwaysOnTopSettings::settings().frameThickness * scalingFactor;
|
||||
float thickness = settings->frameThickness * scalingFactor;
|
||||
float cornerRadius = 0.0;
|
||||
if (AlwaysOnTopSettings::settings().roundCornersEnabled)
|
||||
if (settings->roundCornersEnabled)
|
||||
{
|
||||
cornerRadius = WindowCornerUtils::CornersRadius(m_trackingWindow) * scalingFactor;
|
||||
}
|
||||
@@ -268,7 +269,7 @@ LRESULT WindowBorder::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexce
|
||||
|
||||
void WindowBorder::SettingsUpdate(SettingId id)
|
||||
{
|
||||
if (!AlwaysOnTopSettings::settings().enableFrame)
|
||||
if (!AlwaysOnTopSettings::settings()->enableFrame)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// 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.Common.Helpers;
|
||||
|
||||
public sealed partial class ThrottledDebouncedAction : IDisposable
|
||||
{
|
||||
private static readonly TimeSpan DefaultInterval = TimeSpan.FromMilliseconds(150);
|
||||
|
||||
private readonly Lock _lock = new();
|
||||
private readonly Action _action;
|
||||
private readonly TimeSpan _defaultInterval;
|
||||
private readonly bool _runImmediately;
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _isRunning;
|
||||
private bool _isPending;
|
||||
private TimeSpan _pendingInterval;
|
||||
|
||||
public ThrottledDebouncedAction(Action action)
|
||||
: this(action, DefaultInterval)
|
||||
{
|
||||
}
|
||||
|
||||
public ThrottledDebouncedAction(Action action, TimeSpan interval, bool runImmediately = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(interval, TimeSpan.Zero);
|
||||
|
||||
_action = action;
|
||||
_defaultInterval = interval;
|
||||
_runImmediately = runImmediately;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cancel();
|
||||
}
|
||||
|
||||
public void Invoke() => Invoke(null);
|
||||
|
||||
public void Invoke(TimeSpan? interval)
|
||||
{
|
||||
var effectiveInterval = interval ?? _defaultInterval;
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(effectiveInterval, TimeSpan.Zero);
|
||||
|
||||
if (effectiveInterval == TimeSpan.Zero)
|
||||
{
|
||||
Cancel();
|
||||
_action();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_runImmediately)
|
||||
{
|
||||
// Trailing-edge debounce: each call resets the delay with the new interval.
|
||||
CancellationTokenSource? oldCts;
|
||||
CancellationToken token;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
oldCts = _cts;
|
||||
_cts = new CancellationTokenSource();
|
||||
token = _cts.Token;
|
||||
}
|
||||
|
||||
oldCts?.Cancel();
|
||||
oldCts?.Dispose();
|
||||
|
||||
_ = Task.Run(
|
||||
async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(effectiveInterval, token).ConfigureAwait(false);
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_action();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// expected during reschedules/dispose
|
||||
}
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Leading + Trailing throttle/debounce
|
||||
lock (_lock)
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
_isPending = true;
|
||||
_pendingInterval = effectiveInterval;
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
}
|
||||
|
||||
_action();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
TimeSpan delayInterval;
|
||||
lock (_lock)
|
||||
{
|
||||
// Snapshot the interval to use for this cooldown.
|
||||
// If no pending call yet, use the interval from the
|
||||
// leading invocation; otherwise use the most recent
|
||||
// pending interval (which may be updated by new calls
|
||||
// arriving during the delay).
|
||||
delayInterval = _isPending ? _pendingInterval : effectiveInterval;
|
||||
}
|
||||
|
||||
await Task.Delay(delayInterval).ConfigureAwait(false);
|
||||
|
||||
bool shouldRun;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_isPending)
|
||||
{
|
||||
_isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_isPending = false;
|
||||
shouldRun = true;
|
||||
}
|
||||
|
||||
if (shouldRun)
|
||||
{
|
||||
_action();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void InvokeImmediately() => Invoke(TimeSpan.Zero);
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
CancellationTokenSource? toCancel;
|
||||
lock (_lock)
|
||||
{
|
||||
toCancel = _cts;
|
||||
_cts = null;
|
||||
_isPending = false;
|
||||
_isRunning = false;
|
||||
}
|
||||
|
||||
toCancel?.Cancel();
|
||||
toCancel?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,23 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class AliasManager : ObservableObject
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
// REMEMBER, CommandAlias.SearchPrefix is what we use as keys
|
||||
private readonly Dictionary<string, CommandAlias> _aliases;
|
||||
|
||||
public AliasManager(TopLevelCommandManager tlcManager, SettingsModel settings)
|
||||
public AliasManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_aliases = settings.Aliases;
|
||||
_settingsService = settingsService;
|
||||
_aliases = _settingsService.Settings.Aliases;
|
||||
|
||||
if (_aliases.Count == 0)
|
||||
{
|
||||
|
||||
@@ -1,26 +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.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class AppStateModel : ObservableObject
|
||||
{
|
||||
[JsonIgnore]
|
||||
public static readonly string FilePath;
|
||||
|
||||
public event TypedEventHandler<AppStateModel, object?>? StateChanged;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// STATE HERE
|
||||
// Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)!
|
||||
@@ -31,141 +19,4 @@ public partial class AppStateModel : ObservableObject
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static AppStateModel()
|
||||
{
|
||||
FilePath = StateJsonPath();
|
||||
}
|
||||
|
||||
public static AppStateModel LoadState()
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
|
||||
}
|
||||
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
Debug.WriteLine("The provided settings file does not exist");
|
||||
return new();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
|
||||
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
|
||||
|
||||
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
|
||||
|
||||
return loaded ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.ToString());
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
public static void SaveState(AppStateModel model)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveState)}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Serialize the main dictionary to JSON and save it to the file
|
||||
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel!);
|
||||
|
||||
// validate JSON
|
||||
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
|
||||
{
|
||||
Logger.LogError("Failed to parse app state as a JsonObject.");
|
||||
return;
|
||||
}
|
||||
|
||||
// read previous settings
|
||||
if (!TryReadSavedState(out var savedSettings))
|
||||
{
|
||||
savedSettings = new JsonObject();
|
||||
}
|
||||
|
||||
// merge new settings into old ones
|
||||
foreach (var item in newSettings)
|
||||
{
|
||||
savedSettings[item.Key] = item.Value?.DeepClone();
|
||||
}
|
||||
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
// TODO: Instead of just raising the event here, we should
|
||||
// have a file change watcher on the settings file, and
|
||||
// reload the settings then
|
||||
model.StateChanged?.Invoke(model, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save application state to {FilePath}:", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadSavedState([NotNullWhen(true)] out JsonObject? savedSettings)
|
||||
{
|
||||
savedSettings = null;
|
||||
|
||||
// read existing content from the file
|
||||
string oldContent;
|
||||
try
|
||||
{
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
oldContent = File.ReadAllText(FilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// file doesn't exist (might not have been created yet), so consider this a success
|
||||
// and return empty settings
|
||||
savedSettings = new JsonObject();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to read app state file {FilePath}:\n{ex}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// detect empty file, just for sake of logging
|
||||
if (string.IsNullOrWhiteSpace(oldContent))
|
||||
{
|
||||
Logger.LogInfo($"App state file is empty: {FilePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// is it valid JSON?
|
||||
try
|
||||
{
|
||||
savedSettings = JsonNode.Parse(oldContent) as JsonObject;
|
||||
return savedSettings != null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to parse app state from {FilePath}:\n{ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string StateJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the settings is just next to the exe
|
||||
return Path.Combine(directory, "state.json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -87,7 +87,8 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
Color.FromArgb(255, 126, 115, 95), // #7e735f
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
@@ -100,18 +101,18 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_settings.Theme;
|
||||
get => (int)_settingsService.Settings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _settings.Theme;
|
||||
get => _settingsService.Settings.Theme;
|
||||
set
|
||||
{
|
||||
if (_settings.Theme != value)
|
||||
if (_settingsService.Settings.Theme != value)
|
||||
{
|
||||
_settings.Theme = value;
|
||||
_settingsService.Settings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
@@ -121,12 +122,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _settings.ColorizationMode;
|
||||
get => _settingsService.Settings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_settings.ColorizationMode != value)
|
||||
if (_settingsService.Settings.ColorizationMode != value)
|
||||
{
|
||||
_settings.ColorizationMode = value;
|
||||
_settingsService.Settings.ColorizationMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
@@ -152,18 +153,18 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_settings.ColorizationMode;
|
||||
get => (int)_settingsService.Settings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _settings.CustomThemeColor;
|
||||
get => _settingsService.Settings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_settings.CustomThemeColor != value)
|
||||
if (_settingsService.Settings.CustomThemeColor != value)
|
||||
{
|
||||
_settings.CustomThemeColor = value;
|
||||
_settingsService.Settings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
@@ -179,10 +180,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _settings.CustomThemeColorIntensity;
|
||||
get => _settingsService.Settings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_settings.CustomThemeColorIntensity = value;
|
||||
_settingsService.Settings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
Save();
|
||||
@@ -191,10 +192,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackgroundImageTintIntensity
|
||||
{
|
||||
get => _settings.BackgroundImageTintIntensity;
|
||||
get => _settingsService.Settings.BackgroundImageTintIntensity;
|
||||
set
|
||||
{
|
||||
_settings.BackgroundImageTintIntensity = value;
|
||||
_settingsService.Settings.BackgroundImageTintIntensity = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
Save();
|
||||
@@ -203,12 +204,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _settings.BackgroundImagePath ?? string.Empty;
|
||||
get => _settingsService.Settings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImagePath != value)
|
||||
if (_settingsService.Settings.BackgroundImagePath != value)
|
||||
{
|
||||
_settings.BackgroundImagePath = value;
|
||||
_settingsService.Settings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
@@ -223,12 +224,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _settings.BackgroundImageOpacity;
|
||||
get => _settingsService.Settings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageOpacity != value)
|
||||
if (_settingsService.Settings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_settings.BackgroundImageOpacity = value;
|
||||
_settingsService.Settings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -237,12 +238,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _settings.BackgroundImageBrightness;
|
||||
get => _settingsService.Settings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBrightness != value)
|
||||
if (_settingsService.Settings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_settings.BackgroundImageBrightness = value;
|
||||
_settingsService.Settings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -251,12 +252,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _settings.BackgroundImageBlurAmount;
|
||||
get => _settingsService.Settings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBlurAmount != value)
|
||||
if (_settingsService.Settings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_settings.BackgroundImageBlurAmount = value;
|
||||
_settingsService.Settings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -265,12 +266,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _settings.BackgroundImageFit;
|
||||
get => _settingsService.Settings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageFit != value)
|
||||
if (_settingsService.Settings.BackgroundImageFit != value)
|
||||
{
|
||||
_settings.BackgroundImageFit = value;
|
||||
_settingsService.Settings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
@@ -299,12 +300,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackdropOpacity
|
||||
{
|
||||
get => _settings.BackdropOpacity;
|
||||
get => _settingsService.Settings.BackdropOpacity;
|
||||
set
|
||||
{
|
||||
if (_settings.BackdropOpacity != value)
|
||||
if (_settingsService.Settings.BackdropOpacity != value)
|
||||
{
|
||||
_settings.BackdropOpacity = value;
|
||||
_settingsService.Settings.BackdropOpacity = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveBackdropStyle));
|
||||
OnPropertyChanged(nameof(EffectiveImageOpacity));
|
||||
@@ -315,13 +316,13 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackdropStyleIndex
|
||||
{
|
||||
get => (int)_settings.BackdropStyle;
|
||||
get => (int)_settingsService.Settings.BackdropStyle;
|
||||
set
|
||||
{
|
||||
var newStyle = (BackdropStyle)value;
|
||||
if (_settings.BackdropStyle != newStyle)
|
||||
if (_settingsService.Settings.BackdropStyle != newStyle)
|
||||
{
|
||||
_settings.BackdropStyle = newStyle;
|
||||
_settingsService.Settings.BackdropStyle = newStyle;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
|
||||
@@ -343,25 +344,25 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
/// Gets whether the backdrop opacity slider should be visible.
|
||||
/// </summary>
|
||||
public bool IsBackdropOpacityVisible =>
|
||||
BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
|
||||
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the backdrop description (for styles without options) should be visible.
|
||||
/// </summary>
|
||||
public bool IsMicaBackdropDescriptionVisible =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether background/colorization settings are available.
|
||||
/// </summary>
|
||||
public bool IsBackgroundSettingsEnabled =>
|
||||
BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
|
||||
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
|
||||
/// </summary>
|
||||
public bool IsBackgroundNotAvailableVisible =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
|
||||
|
||||
public BackdropStyle? EffectiveBackdropStyle
|
||||
{
|
||||
@@ -370,9 +371,9 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
// Return style when transparency/blur is visible (not fully opaque Acrylic)
|
||||
// - Clear/Mica/MicaAlt/AcrylicThin always show their effect
|
||||
// - Acrylic shows effect only when opacity < 100
|
||||
if (_settings.BackdropStyle != BackdropStyle.Acrylic || _settings.BackdropOpacity < 100)
|
||||
if (_settingsService.Settings.BackdropStyle != BackdropStyle.Acrylic || _settingsService.Settings.BackdropOpacity < 100)
|
||||
{
|
||||
return _settings.BackdropStyle;
|
||||
return _settingsService.Settings.BackdropStyle;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -381,39 +382,39 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public double EffectiveImageOpacity =>
|
||||
EffectiveBackdropStyle is not null
|
||||
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settings.BackdropOpacity / 100.0)
|
||||
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settingsService.Settings.BackdropOpacity / 100.0)
|
||||
: (BackgroundImageOpacity / 100f);
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
public bool IsCustomTintVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
|
||||
public bool IsColorIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public bool IsImageTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsImageTintIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective tint intensity for the preview, based on the current colorization mode.
|
||||
/// </summary>
|
||||
public int EffectiveTintIntensity => _settings.ColorizationMode is ColorizationMode.Image
|
||||
? _settings.BackgroundImageTintIntensity
|
||||
: _settings.CustomThemeColorIntensity;
|
||||
public int EffectiveTintIntensity => _settingsService.Settings.ColorizationMode is ColorizationMode.Image
|
||||
? _settingsService.Settings.BackgroundImageTintIntensity
|
||||
: _settingsService.Settings.CustomThemeColorIntensity;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsBackgroundControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
|
||||
public bool IsNoBackgroundVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
public bool IsAccentColorControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public bool IsResetButtonVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsResetButtonVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public BackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization
|
||||
? Colors.Transparent
|
||||
: ColorizationMode switch
|
||||
{
|
||||
@@ -428,7 +429,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsBackgroundImage
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsBackgroundImage
|
||||
? null
|
||||
: ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
@@ -436,11 +437,11 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, ISettingsService settingsService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
@@ -448,7 +449,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
|
||||
IsColorizationDetailsExpanded = _settingsService.Settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
@@ -469,7 +470,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_settingsService.Save();
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -15,7 +15,7 @@ internal static class BatchUpdateManager
|
||||
// 30 ms chosen empirically to balance responsiveness and batching:
|
||||
// - Keeps perceived latency low (< ~50 ms) for user-visible updates.
|
||||
// - Still allows multiple COM/background events to be coalesced into a single batch.
|
||||
private static readonly TimeSpan BatchDelay = TimeSpan.FromMilliseconds(30);
|
||||
private static readonly TimeSpan BatchDelay = TimeSpan.FromMilliseconds(40);
|
||||
private static readonly ConcurrentQueue<IBatchUpdateTarget> DirtyQueue = [];
|
||||
private static readonly Timer Timer = new(static _ => Flush(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
|
||||
|
||||
|
||||
@@ -122,11 +122,9 @@ public sealed partial class CommandBarViewModel : ObservableObject,
|
||||
}
|
||||
|
||||
SecondaryCommand = SelectedItem.SecondaryCommand;
|
||||
var moreCommands = SelectedItem.MoreCommands;
|
||||
|
||||
var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
|
||||
var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
|
||||
|
||||
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
|
||||
ShouldShowContextMenu = moreCommands.Count > 1 && SelectedItem.HasMoreCommands;
|
||||
|
||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||
OnPropertyChanged(nameof(SecondaryCommand));
|
||||
|
||||
@@ -21,6 +21,12 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
private readonly IContextMenuFactory? _contextMenuFactory;
|
||||
|
||||
private readonly Lock _moreCommandsLock = new();
|
||||
private readonly List<IContextItemViewModel> _moreCommands = [];
|
||||
private volatile CommandContextItemViewModel? _secondaryMoreCommand;
|
||||
private volatile IContextItemViewModel[] _moreCommandsSnapshot = [];
|
||||
private volatile IContextItemViewModel[] _allCommandsSnapshot = [];
|
||||
|
||||
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
|
||||
|
||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||
@@ -63,33 +69,22 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public CommandViewModel Command { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> MoreCommands { get; private set; } = [];
|
||||
// Reuse a cached read-only snapshot so repeated reads don't allocate.
|
||||
public IReadOnlyList<IContextItemViewModel> MoreCommands => _moreCommandsSnapshot;
|
||||
|
||||
IEnumerable<IContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
|
||||
IReadOnlyList<IContextItemViewModel> IContextMenuContext.MoreCommands => _moreCommandsSnapshot;
|
||||
|
||||
private List<CommandContextItemViewModel> ActualCommands => MoreCommands.OfType<CommandContextItemViewModel>().ToList();
|
||||
protected Lock MoreCommandsLock => _moreCommandsLock;
|
||||
|
||||
public bool HasMoreCommands => ActualCommands.Count > 0;
|
||||
protected List<IContextItemViewModel> UnsafeMoreCommands => _moreCommands;
|
||||
|
||||
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
|
||||
public bool HasMoreCommands => _secondaryMoreCommand is not null;
|
||||
|
||||
public string SecondaryCommandName => _secondaryMoreCommand?.Name ?? string.Empty;
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand => this;
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasMoreCommands)
|
||||
{
|
||||
if (MoreCommands[0] is CommandContextItemViewModel command)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public CommandItemViewModel? SecondaryCommand => _secondaryMoreCommand;
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
@@ -101,18 +96,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
{
|
||||
get
|
||||
{
|
||||
List<IContextItemViewModel> l = _defaultCommandContextItemViewModel is null ?
|
||||
new() :
|
||||
[_defaultCommandContextItemViewModel];
|
||||
|
||||
l.AddRange(MoreCommands);
|
||||
return l;
|
||||
}
|
||||
}
|
||||
public IReadOnlyList<IContextItemViewModel> AllCommands => _allCommandsSnapshot;
|
||||
|
||||
private static readonly IconInfoViewModel _errorIcon;
|
||||
|
||||
@@ -246,6 +230,11 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateDefaultContextItemIcon();
|
||||
}
|
||||
|
||||
lock (_moreCommandsLock)
|
||||
{
|
||||
RefreshMoreCommandStateUnsafe();
|
||||
}
|
||||
|
||||
Initialized |= InitializedState.SelectionInitialized;
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
@@ -265,7 +254,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
Command = new(null, PageContext);
|
||||
_itemTitle = "Error";
|
||||
Subtitle = "Item failed to load";
|
||||
MoreCommands = [];
|
||||
ClearMoreCommands();
|
||||
_icon = _errorIcon;
|
||||
_titleCache.Invalidate();
|
||||
_subtitleCache.Invalidate();
|
||||
@@ -304,7 +293,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
Command = new(null, PageContext);
|
||||
_itemTitle = "Error";
|
||||
Subtitle = "Item failed to load";
|
||||
MoreCommands = [];
|
||||
ClearMoreCommands();
|
||||
_icon = _errorIcon;
|
||||
_titleCache.Invalidate();
|
||||
_subtitleCache.Invalidate();
|
||||
@@ -385,9 +374,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
case nameof(model.MoreCommands):
|
||||
BuildAndInitMoreCommands();
|
||||
UpdateProperty(nameof(SecondaryCommand));
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands));
|
||||
|
||||
break;
|
||||
case nameof(DataPackage):
|
||||
@@ -478,9 +465,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
var results = factory.UnsafeBuildAndInitMoreCommands(more, this);
|
||||
|
||||
List<IContextItemViewModel>? freedItems;
|
||||
lock (MoreCommands)
|
||||
lock (_moreCommandsLock)
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(MoreCommands, results, out freedItems);
|
||||
ListHelpers.InPlaceUpdateList(_moreCommands, results, out freedItems);
|
||||
RefreshMoreCommandStateUnsafe();
|
||||
}
|
||||
|
||||
freedItems.OfType<CommandContextItemViewModel>()
|
||||
@@ -516,20 +504,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
lock (MoreCommands)
|
||||
List<IContextItemViewModel> freedItems;
|
||||
CommandContextItemViewModel? freedDefault;
|
||||
lock (_moreCommandsLock)
|
||||
{
|
||||
MoreCommands.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(c => c.SafeCleanup());
|
||||
MoreCommands.Clear();
|
||||
freedItems = [.. _moreCommands];
|
||||
_moreCommands.Clear();
|
||||
|
||||
// Null out here so the single RefreshMoreCommandStateUnsafe call
|
||||
// produces an _allCommandsSnapshot that excludes the default command.
|
||||
freedDefault = _defaultCommandContextItemViewModel;
|
||||
_defaultCommandContextItemViewModel = null;
|
||||
|
||||
RefreshMoreCommandStateUnsafe();
|
||||
}
|
||||
|
||||
// Cleanup outside lock to avoid holding it during RPC calls
|
||||
freedItems.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(c => c.SafeCleanup());
|
||||
freedDefault?.SafeCleanup();
|
||||
|
||||
// _listItemIcon.SafeCleanup();
|
||||
_icon = new(null); // necessary?
|
||||
|
||||
_defaultCommandContextItemViewModel?.SafeCleanup();
|
||||
_defaultCommandContextItemViewModel = null;
|
||||
|
||||
Command.PropertyChanged -= Command_PropertyChanged;
|
||||
Command.SafeCleanup();
|
||||
|
||||
@@ -545,6 +543,40 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
base.SafeCleanup();
|
||||
Initialized |= InitializedState.CleanedUp;
|
||||
}
|
||||
|
||||
protected void RefreshMoreCommandStateUnsafe()
|
||||
{
|
||||
_moreCommandsSnapshot = [.. _moreCommands];
|
||||
|
||||
_secondaryMoreCommand = null;
|
||||
foreach (var item in _moreCommands)
|
||||
{
|
||||
if (item is CommandContextItemViewModel command)
|
||||
{
|
||||
_secondaryMoreCommand = command;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_allCommandsSnapshot = _defaultCommandContextItemViewModel is null ?
|
||||
_moreCommandsSnapshot :
|
||||
[_defaultCommandContextItemViewModel, .. _moreCommandsSnapshot];
|
||||
}
|
||||
|
||||
private void ClearMoreCommands()
|
||||
{
|
||||
List<IContextItemViewModel> freedItems;
|
||||
lock (_moreCommandsLock)
|
||||
{
|
||||
freedItems = [.. _moreCommands];
|
||||
_moreCommands.Clear();
|
||||
RefreshMoreCommandStateUnsafe();
|
||||
}
|
||||
|
||||
freedItems.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(c => c.SafeCleanup());
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -22,6 +23,7 @@ public class CommandPalettePageViewModelFactory
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
MainListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested, IsMainPage = true },
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested },
|
||||
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
|
||||
_ => null,
|
||||
|
||||
@@ -139,7 +139,8 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var settings = settingsService.Settings;
|
||||
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
IsActive = providerSettings.IsEnabled;
|
||||
@@ -249,16 +250,15 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
IServiceProvider serviceProvider,
|
||||
ICommandProvider4? four)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settings = serviceProvider.GetRequiredService<ISettingsService>().Settings;
|
||||
var contextMenuFactory = serviceProvider.GetService<IContextMenuFactory>()!;
|
||||
var state = serviceProvider.GetService<AppStateModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
var ourContext = GetProviderContext();
|
||||
WeakReference<IPageContext> pageContext = new(this.TopLevelPageContext);
|
||||
var make = (ICommandItem? i, TopLevelType t) =>
|
||||
{
|
||||
CommandItemViewModel commandItemViewModel = new(new(i), pageContext, contextMenuFactory: contextMenuFactory);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
|
||||
topLevelViewModel.InitializeProperties();
|
||||
|
||||
return topLevelViewModel;
|
||||
@@ -407,7 +407,8 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
|
||||
public void PinCommand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var settings = settingsService.Settings;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
|
||||
if (!providerSettings.PinnedCommandIds.Contains(commandId))
|
||||
@@ -416,13 +417,14 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
settingsService.Save(hotReload: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var settings = settingsService.Settings;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
|
||||
if (providerSettings.PinnedCommandIds.Remove(commandId))
|
||||
@@ -430,13 +432,14 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
settingsService.Save(hotReload: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void PinDockBand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var settings = settingsService.Settings;
|
||||
var bandSettings = new DockBandSettings
|
||||
{
|
||||
CommandId = commandId,
|
||||
@@ -447,19 +450,20 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
settingsService.Save(hotReload: false);
|
||||
}
|
||||
|
||||
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var settings = settingsService.Settings;
|
||||
settings.DockSettings.StartBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
settings.DockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
settings.DockSettings.EndBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
settingsService.Save(hotReload: false);
|
||||
}
|
||||
|
||||
public ICommandProviderContext GetProviderContext() => this;
|
||||
|
||||
@@ -2,18 +2,23 @@
|
||||
// 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.Immutable;
|
||||
/*
|
||||
#define CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
|
||||
*/
|
||||
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -25,12 +30,21 @@ namespace Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
/// </summary>
|
||||
public sealed partial class MainListPage : DynamicListPage,
|
||||
IRecipient<ClearSearchMessage>,
|
||||
IRecipient<UpdateFallbackItemsMessage>, IDisposable
|
||||
IRecipient<UpdateFallbackItemsMessage>,
|
||||
IDisposable
|
||||
{
|
||||
// Throttle for raising items changed events from external sources
|
||||
private static readonly TimeSpan RaiseItemsChangedThrottle = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
// Throttle for raising items changed events from user input - we want this to feel more responsive, so a shorter throttle.
|
||||
private static readonly TimeSpan RaiseItemsChangedThrottleForUserInput = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
private readonly FallbackUpdateManager _fallbackUpdateManager;
|
||||
private readonly ThrottledDebouncedAction _refreshThrottledDebouncedAction;
|
||||
private readonly TopLevelCommandManager _tlcManager;
|
||||
private readonly AliasManager _aliasManager;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly AppStateModel _appStateModel;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly ScoringFunction<IListItem> _scoringFunction;
|
||||
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
|
||||
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
|
||||
@@ -54,50 +68,91 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
|
||||
private int AppResultLimit => AllAppsCommandProvider.TopLevelResultLimit;
|
||||
|
||||
private InterlockedBoolean _fullRefreshRequested;
|
||||
private InterlockedBoolean _refreshRunning;
|
||||
private InterlockedBoolean _refreshRequested;
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
|
||||
private DateTimeOffset _last = DateTimeOffset.UtcNow;
|
||||
#endif
|
||||
|
||||
public MainListPage(
|
||||
TopLevelCommandManager topLevelCommandManager,
|
||||
SettingsModel settings,
|
||||
AliasManager aliasManager,
|
||||
AppStateModel appStateModel,
|
||||
IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||
IFuzzyMatcherProvider fuzzyMatcherProvider,
|
||||
ISettingsService settingsService,
|
||||
IAppStateService appStateService)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.home";
|
||||
Title = Resources.builtin_home_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
|
||||
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
|
||||
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
_aliasManager = aliasManager;
|
||||
_appStateModel = appStateModel;
|
||||
_appStateService = appStateService;
|
||||
_tlcManager = topLevelCommandManager;
|
||||
_fuzzyMatcherProvider = fuzzyMatcherProvider;
|
||||
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateModel.RecentCommands, _fuzzyMatcherProvider.Current);
|
||||
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settings.FallbackRanks);
|
||||
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateService.State.RecentCommands, _fuzzyMatcherProvider.Current);
|
||||
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settingsService.Settings.FallbackRanks);
|
||||
|
||||
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
|
||||
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
|
||||
|
||||
_refreshThrottledDebouncedAction = new ThrottledDebouncedAction(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
|
||||
var delta = DateTimeOffset.UtcNow - _last;
|
||||
_last = DateTimeOffset.UtcNow;
|
||||
Logger.LogDebug($"UpdateFallbacks: RaiseItemsChanged, delta {delta}");
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
#endif
|
||||
if (_fullRefreshRequested.Clear())
|
||||
{
|
||||
// full refresh
|
||||
RaiseItemsChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
// preserve selection
|
||||
RaiseItemsChanged(ListViewModel.IncrementalRefresh);
|
||||
}
|
||||
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
|
||||
Logger.LogInfo($"UpdateFallbacks: RaiseItemsChanged took {sw.Elapsed}");
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Unhandled exception in MainListPage refresh debounced action", ex);
|
||||
}
|
||||
},
|
||||
RaiseItemsChangedThrottle);
|
||||
|
||||
_fallbackUpdateManager = new FallbackUpdateManager(() => RequestRefresh(fullRefresh: false));
|
||||
|
||||
// The all apps page will kick off a BG thread to start loading apps.
|
||||
// We just want to know when it is done.
|
||||
var allApps = AllAppsCommandProvider.Page;
|
||||
allApps.PropChanged += (s, p) =>
|
||||
{
|
||||
if (p.PropertyName == nameof(allApps.IsLoading))
|
||||
{
|
||||
if (p.PropertyName == nameof(allApps.IsLoading))
|
||||
{
|
||||
IsLoading = ActuallyLoading();
|
||||
}
|
||||
};
|
||||
IsLoading = ActuallyLoading();
|
||||
}
|
||||
};
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
|
||||
|
||||
settings.SettingsChanged += SettingsChangedHandler;
|
||||
HotReloadSettings(settings);
|
||||
_settingsService.SettingsChanged += SettingsChangedHandler;
|
||||
HotReloadSettings(_settingsService.Settings);
|
||||
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
|
||||
|
||||
IsLoading = true;
|
||||
@@ -120,10 +175,20 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseItemsChanged(ListViewModel.IncrementalRefresh);
|
||||
RequestRefresh(fullRefresh: false);
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestRefresh(bool fullRefresh, TimeSpan? interval = null)
|
||||
{
|
||||
if (fullRefresh)
|
||||
{
|
||||
_fullRefreshRequested.Set();
|
||||
}
|
||||
|
||||
_refreshThrottledDebouncedAction.Invoke(interval);
|
||||
}
|
||||
|
||||
private void ReapplySearchInBackground()
|
||||
{
|
||||
_refreshRequested.Set();
|
||||
@@ -151,7 +216,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
|
||||
var currentSearchText = SearchText;
|
||||
UpdateSearchText(currentSearchText, currentSearchText);
|
||||
UpdateSearchTextCore(currentSearchText, currentSearchText, isUserInput: false);
|
||||
}
|
||||
while (_refreshRequested.Value);
|
||||
}
|
||||
@@ -243,6 +308,11 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
UpdateSearchTextCore(oldSearch, newSearch, isUserInput: true);
|
||||
}
|
||||
|
||||
private void UpdateSearchTextCore(string oldSearch, string newSearch, bool isUserInput)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
@@ -295,9 +365,9 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
|
||||
// prefilter fallbacks
|
||||
var globalFallbacks = _settings.GetGlobalFallbacks();
|
||||
var globalFallbacks = _settingsService.Settings.GetGlobalFallbacks();
|
||||
var specialFallbacks = new List<TopLevelViewModel>(globalFallbacks.Length);
|
||||
var commonFallbacks = new List<TopLevelViewModel>();
|
||||
var commonFallbacks = new List<TopLevelViewModel>(commands.Count - globalFallbacks.Length);
|
||||
|
||||
foreach (var s in commands)
|
||||
{
|
||||
@@ -316,10 +386,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
// start update of fallbacks; update special fallbacks separately,
|
||||
// so they can finish faster
|
||||
UpdateFallbacks(SearchText, specialFallbacks, token);
|
||||
UpdateFallbacks(SearchText, commonFallbacks, token);
|
||||
_fallbackUpdateManager.BeginUpdate(SearchText, [.. specialFallbacks, .. commonFallbacks], token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -327,11 +394,13 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
|
||||
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
|
||||
if (string.IsNullOrEmpty(newSearch))
|
||||
if (string.IsNullOrWhiteSpace(newSearch))
|
||||
{
|
||||
_filteredItemsIncludesApps = _includeApps;
|
||||
ClearResults();
|
||||
RaiseItemsChanged();
|
||||
var wasAlreadyEmpty = string.IsNullOrWhiteSpace(oldSearch);
|
||||
RequestRefresh(fullRefresh: true, interval: wasAlreadyEmpty ? null : TimeSpan.Zero);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,7 +480,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
|
||||
// We need to remove pinned apps from allNewApps so they don't show twice.
|
||||
// Pinned app command IDs are stored in ProviderSettings.PinnedCommandIds.
|
||||
_settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
|
||||
_settingsService.Settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
|
||||
var pinnedCommandIds = providerSettings?.PinnedCommandIds;
|
||||
|
||||
if (pinnedCommandIds is not null && pinnedCommandIds.Count > 0)
|
||||
@@ -466,49 +535,35 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
}
|
||||
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
|
||||
var filterDoneTimestamp = stopwatch.ElapsedMilliseconds;
|
||||
Logger.LogDebug($"Filter with '{newSearch}' in {filterDoneTimestamp}ms");
|
||||
#endif
|
||||
if (isUserInput)
|
||||
{
|
||||
// Make sure that the throttle delay is consistent from the user's perspective, even if filtering
|
||||
// takes a long time. If we always use the full throttle duration, then a slow filter could make the UI feel sluggish.
|
||||
var adjustedInterval = RaiseItemsChangedThrottleForUserInput - stopwatch.Elapsed;
|
||||
if (adjustedInterval < TimeSpan.Zero)
|
||||
{
|
||||
adjustedInterval = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
RaiseItemsChanged();
|
||||
RequestRefresh(fullRefresh: true, adjustedInterval);
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestRefresh(fullRefresh: true);
|
||||
}
|
||||
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
|
||||
var listPageUpdatedTimestamp = stopwatch.ElapsedMilliseconds;
|
||||
Logger.LogDebug($"Render items with '{newSearch}' in {listPageUpdatedTimestamp}ms /d {listPageUpdatedTimestamp - filterDoneTimestamp}ms");
|
||||
#endif
|
||||
|
||||
stopwatch.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFallbacks(string newSearch, IReadOnlyList<TopLevelViewModel> commands, CancellationToken token)
|
||||
{
|
||||
_ = Task.Run(
|
||||
() =>
|
||||
{
|
||||
var needsToUpdate = false;
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch);
|
||||
needsToUpdate = needsToUpdate || changedVisibility;
|
||||
}
|
||||
|
||||
if (needsToUpdate)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RaiseItemsChanged(ListViewModel.IncrementalRefresh);
|
||||
}
|
||||
},
|
||||
token);
|
||||
}
|
||||
|
||||
private bool ActuallyLoading()
|
||||
{
|
||||
var allApps = AllAppsCommandProvider.Page;
|
||||
@@ -624,9 +679,9 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
public void UpdateHistory(IListItem topLevelOrAppItem)
|
||||
{
|
||||
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
||||
var history = _appStateModel.RecentCommands;
|
||||
var history = _appStateService.State.RecentCommands;
|
||||
history.AddHistoryItem(id);
|
||||
AppStateModel.SaveState(_appStateModel);
|
||||
_appStateService.Save();
|
||||
}
|
||||
|
||||
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
@@ -644,9 +699,12 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
|
||||
public void Receive(ClearSearchMessage message) => SearchText = string.Empty;
|
||||
|
||||
public void Receive(UpdateFallbackItemsMessage message) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
|
||||
public void Receive(UpdateFallbackItemsMessage message)
|
||||
{
|
||||
RequestRefresh(fullRefresh: false);
|
||||
}
|
||||
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender);
|
||||
private void SettingsChangedHandler(ISettingsService sender, SettingsModel args) => HotReloadSettings(args);
|
||||
|
||||
private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails;
|
||||
|
||||
@@ -654,13 +712,14 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_fallbackUpdateManager.Dispose();
|
||||
|
||||
_tlcManager.PropertyChanged -= TlcManager_PropertyChanged;
|
||||
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
|
||||
|
||||
if (_settings is not null)
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
_settings.SettingsChanged -= SettingsChangedHandler;
|
||||
_settingsService.SettingsChanged -= SettingsChangedHandler;
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
|
||||
@@ -30,7 +30,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": {{FormatJsonString(Properties.Resources.builtin_create_extension_page_title)}},
|
||||
"size": "large"
|
||||
"size": "medium",
|
||||
"weight": "bolder"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
@@ -122,9 +123,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage);
|
||||
|
||||
_creatingMessage.State = MessageState.Error;
|
||||
_creatingMessage.Progress = null;
|
||||
_creatingMessage.Message = $"Error: {e.Message}";
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,15 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
{
|
||||
private readonly ExtensionObject<IContentPage> _model;
|
||||
private readonly Lock _commandsLock = new();
|
||||
private volatile CommandSnapshot _snapshot = CommandSnapshot.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ContentViewModel> Content { get; set; } = [];
|
||||
|
||||
public List<IContextItemViewModel> Commands { get; private set; } = [];
|
||||
private List<IContextItemViewModel> Commands { get; } = [];
|
||||
|
||||
public bool HasCommands => ActualCommands.Count > 0;
|
||||
public bool HasCommands => _snapshot.PrimaryCommand is not null;
|
||||
|
||||
public DetailsViewModel? Details { get; private set; }
|
||||
|
||||
@@ -31,19 +33,17 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
public bool HasDetails => Details is not null;
|
||||
|
||||
/////// ICommandBarContext ///////
|
||||
public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
|
||||
public IReadOnlyList<IContextItemViewModel> MoreCommands => _snapshot.MoreCommands;
|
||||
|
||||
private List<CommandContextItemViewModel> ActualCommands => Commands.OfType<CommandContextItemViewModel>().ToList();
|
||||
public bool HasMoreCommands => _snapshot.SecondaryCommand is not null;
|
||||
|
||||
public bool HasMoreCommands => ActualCommands.Count > 1;
|
||||
public string SecondaryCommandName => _snapshot.SecondaryCommand?.Name ?? string.Empty;
|
||||
|
||||
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
|
||||
public CommandItemViewModel? PrimaryCommand => _snapshot.PrimaryCommand;
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand => HasCommands ? ActualCommands[0] : null;
|
||||
public CommandItemViewModel? SecondaryCommand => _snapshot.SecondaryCommand;
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[1] : null;
|
||||
|
||||
public List<IContextItemViewModel> AllCommands => Commands;
|
||||
public IReadOnlyList<IContextItemViewModel> AllCommands => _snapshot.AllCommands;
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
@@ -109,28 +109,14 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
Commands = model.Commands
|
||||
.ToList()
|
||||
.Select<IContextItem, IContextItemViewModel>(item =>
|
||||
{
|
||||
if (item is ICommandContextItem contextItem)
|
||||
{
|
||||
return new CommandContextItemViewModel(contextItem, PageContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorViewModel();
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
var commands = BuildCommandViewModels(model.Commands);
|
||||
InitializeCommandViewModels(commands, static contextItem => contextItem.InitializeProperties());
|
||||
|
||||
Commands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(contextItem =>
|
||||
{
|
||||
contextItem.InitializeProperties();
|
||||
});
|
||||
lock (_commandsLock)
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Commands, commands);
|
||||
RefreshCommandSnapshotsUnsafe();
|
||||
}
|
||||
|
||||
var extensionDetails = model.Details;
|
||||
if (extensionDetails is not null)
|
||||
@@ -168,37 +154,29 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
var more = model.Commands;
|
||||
if (more is not null)
|
||||
{
|
||||
var newContextMenu = more
|
||||
.ToList()
|
||||
.Select(item =>
|
||||
{
|
||||
if (item is ICommandContextItem contextItem)
|
||||
{
|
||||
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorViewModel();
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
var newContextMenu = BuildCommandViewModels(more);
|
||||
InitializeCommandViewModels(newContextMenu, static contextItem => contextItem.SlowInitializeProperties());
|
||||
|
||||
lock (Commands)
|
||||
List<IContextItemViewModel> removedItems;
|
||||
lock (_commandsLock)
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Commands, newContextMenu);
|
||||
ListHelpers.InPlaceUpdateList(Commands, newContextMenu, out removedItems);
|
||||
RefreshCommandSnapshotsUnsafe();
|
||||
}
|
||||
|
||||
Commands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(contextItem =>
|
||||
{
|
||||
contextItem.SlowInitializeProperties();
|
||||
});
|
||||
CleanupCommandViewModels(removedItems);
|
||||
}
|
||||
else
|
||||
{
|
||||
Commands.Clear();
|
||||
List<IContextItemViewModel> removedItems;
|
||||
lock (_commandsLock)
|
||||
{
|
||||
removedItems = [.. Commands];
|
||||
Commands.Clear();
|
||||
RefreshCommandSnapshotsUnsafe();
|
||||
}
|
||||
|
||||
CleanupCommandViewModels(removedItems);
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(PrimaryCommand));
|
||||
@@ -206,6 +184,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasCommands));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
DoOnUiThread(
|
||||
() =>
|
||||
@@ -243,6 +222,72 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
});
|
||||
}
|
||||
|
||||
private List<IContextItemViewModel> BuildCommandViewModels(IContextItem[]? items)
|
||||
{
|
||||
if (items is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return items
|
||||
.Select<IContextItem, IContextItemViewModel>(item =>
|
||||
{
|
||||
if (item is ICommandContextItem contextItem)
|
||||
{
|
||||
return new CommandContextItemViewModel(contextItem, PageContext);
|
||||
}
|
||||
|
||||
return new SeparatorViewModel();
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static void InitializeCommandViewModels(IEnumerable<IContextItemViewModel> commands, Action<CommandContextItemViewModel> initialize)
|
||||
{
|
||||
foreach (var contextItem in commands.OfType<CommandContextItemViewModel>())
|
||||
{
|
||||
initialize(contextItem);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CleanupCommandViewModels(IEnumerable<IContextItemViewModel> commands)
|
||||
{
|
||||
foreach (var contextItem in commands.OfType<CommandContextItemViewModel>())
|
||||
{
|
||||
contextItem.SafeCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCommandSnapshotsUnsafe()
|
||||
{
|
||||
var allCommands = (IContextItemViewModel[])[.. Commands];
|
||||
var moreCommands = allCommands.Length > 1
|
||||
? allCommands[1..]
|
||||
: [];
|
||||
|
||||
CommandContextItemViewModel? primary = null;
|
||||
CommandContextItemViewModel? secondary = null;
|
||||
foreach (var item in allCommands)
|
||||
{
|
||||
if (item is not CommandContextItemViewModel command)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (primary is null)
|
||||
{
|
||||
primary = command;
|
||||
}
|
||||
else if (secondary is null)
|
||||
{
|
||||
secondary = command;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_snapshot = new(allCommands, moreCommands, primary, secondary);
|
||||
}
|
||||
|
||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||
// this comes in on Enter keypresses in the SearchBox
|
||||
[RelayCommand]
|
||||
@@ -270,12 +315,15 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
|
||||
Details?.SafeCleanup();
|
||||
|
||||
Commands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.ToList()
|
||||
.ForEach(item => item.SafeCleanup());
|
||||
List<IContextItemViewModel> removedItems;
|
||||
lock (_commandsLock)
|
||||
{
|
||||
removedItems = [.. Commands];
|
||||
Commands.Clear();
|
||||
RefreshCommandSnapshotsUnsafe();
|
||||
}
|
||||
|
||||
Commands.Clear();
|
||||
CleanupCommandViewModels(removedItems);
|
||||
|
||||
foreach (var item in Content)
|
||||
{
|
||||
@@ -290,4 +338,25 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
model.ItemsChanged -= Model_ItemsChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable bundle of derived command state, published atomically via a
|
||||
/// single volatile write so readers never see a torn snapshot.
|
||||
/// </summary>
|
||||
private sealed class CommandSnapshot(
|
||||
IContextItemViewModel[] allCommands,
|
||||
IContextItemViewModel[] moreCommands,
|
||||
CommandContextItemViewModel? primaryCommand,
|
||||
CommandContextItemViewModel? secondaryCommand)
|
||||
{
|
||||
public static CommandSnapshot Empty { get; } = new([], [], null, null);
|
||||
|
||||
public IContextItemViewModel[] AllCommands { get; } = allCommands;
|
||||
|
||||
public IContextItemViewModel[] MoreCommands { get; } = moreCommands;
|
||||
|
||||
public CommandContextItemViewModel? PrimaryCommand { get; } = primaryCommand;
|
||||
|
||||
public CommandContextItemViewModel? SecondaryCommand { get; } = secondaryCommand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
// 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.Concurrent;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// An elastic pool of dedicated background threads for running blocking work
|
||||
/// off the ThreadPool. Starts with <c>minThreads</c> always-alive threads and
|
||||
/// expands up to <c>maxThreads</c> on demand. Threads above the minimum exit
|
||||
/// automatically after <c>idleTimeout</c> with no work. Items are processed
|
||||
/// FIFO; cancelled items are skipped at dequeue time.
|
||||
/// </summary>
|
||||
internal sealed partial class DedicatedThreadPool : IDisposable
|
||||
{
|
||||
private const int DrainTimeoutMs = 3000;
|
||||
|
||||
private readonly BlockingCollection<Action> _workQueue = new();
|
||||
private readonly int _minThreads;
|
||||
private readonly int _maxThreads;
|
||||
private readonly TimeSpan _idleTimeout;
|
||||
private readonly string _name;
|
||||
|
||||
// Total live threads (Interlocked). Owned by the thread that wins the CAS.
|
||||
private int _threadCount;
|
||||
|
||||
// Threads currently blocked in TryTake waiting for work (Interlocked).
|
||||
// Used as the expansion trigger: if zero, all threads are busy.
|
||||
private int _idleCount;
|
||||
|
||||
// Ever-increasing counter for unique thread names across expand/shrink cycles.
|
||||
private int _nextThreadId;
|
||||
|
||||
private InterlockedBoolean _disposed;
|
||||
|
||||
public DedicatedThreadPool(int minThreads, int maxThreads, string name = "DedicatedWorker", TimeSpan? idleTimeout = null)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minThreads);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maxThreads, minThreads);
|
||||
|
||||
_minThreads = minThreads;
|
||||
_maxThreads = maxThreads;
|
||||
_name = name;
|
||||
_idleTimeout = idleTimeout ?? TimeSpan.FromSeconds(30);
|
||||
|
||||
_threadCount = minThreads;
|
||||
for (var i = 0; i < minThreads; i++)
|
||||
{
|
||||
StartThread();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartThread()
|
||||
{
|
||||
var id = Interlocked.Increment(ref _nextThreadId);
|
||||
var thread = new Thread(WorkerLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"{_name}-{id}",
|
||||
Priority = ThreadPriority.BelowNormal,
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private void WorkerLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Interlocked.Increment(ref _idleCount);
|
||||
|
||||
bool got;
|
||||
Action? action;
|
||||
try
|
||||
{
|
||||
got = _workQueue.TryTake(out action, _idleTimeout);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Pool was disposed while we were waiting.
|
||||
Interlocked.Decrement(ref _idleCount);
|
||||
Interlocked.Decrement(ref _threadCount);
|
||||
return;
|
||||
}
|
||||
|
||||
Interlocked.Decrement(ref _idleCount);
|
||||
|
||||
if (got)
|
||||
{
|
||||
try
|
||||
{
|
||||
action!();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// QueueAsync wraps work in its own try-catch, so this should
|
||||
// never fire. Keep the thread alive defensively.
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// TryTake timed out (no work for idleTimeout).
|
||||
if (_workQueue.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to shrink: exit if we're above the minimum.
|
||||
// CAS ensures exactly one thread wins each decrement race.
|
||||
while (true)
|
||||
{
|
||||
var count = _threadCount;
|
||||
if (count <= _minThreads)
|
||||
{
|
||||
break; // At minimum — stay alive.
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _threadCount, count - 1, count) == count)
|
||||
{
|
||||
return; // Decremented successfully — this thread exits.
|
||||
}
|
||||
|
||||
// Another thread changed _threadCount concurrently; retry.
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Decrement(ref _threadCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a blocking work item. Returns a <see cref="Task"/> that
|
||||
/// completes when the work finishes on a dedicated thread.
|
||||
/// If <paramref name="cancellationToken"/> is already cancelled when
|
||||
/// the item reaches the front of the queue, it is skipped immediately.
|
||||
/// Spawns an extra thread (up to <c>maxThreads</c>) if all current
|
||||
/// threads are occupied.
|
||||
/// </summary>
|
||||
public Task QueueAsync(Action work, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
try
|
||||
{
|
||||
_workQueue.Add(
|
||||
() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
tcs.TrySetCanceled(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
work();
|
||||
tcs.TrySetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
// If no thread is idle, all are blocked in COM calls — try to expand.
|
||||
if (Volatile.Read(ref _idleCount) == 0)
|
||||
{
|
||||
TryExpand();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
tcs.TrySetCanceled(cancellationToken);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
tcs.TrySetCanceled(CancellationToken.None);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// CompleteAdding was called — pool is shutting down.
|
||||
tcs.TrySetCanceled(CancellationToken.None);
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a blocking work item. Returns a <see cref="Task{T}"/> that
|
||||
/// completes when the work finishes on a dedicated thread.
|
||||
/// If <paramref name="cancellationToken"/> is already cancelled when
|
||||
/// the item reaches the front of the queue, it is skipped immediately.
|
||||
/// Spawns an extra thread (up to <c>maxThreads</c>) if all current
|
||||
/// threads are occupied.
|
||||
/// </summary>
|
||||
public Task<T> QueueAsync<T>(Func<T> work, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
try
|
||||
{
|
||||
_workQueue.Add(
|
||||
() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
tcs.TrySetCanceled(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
tcs.TrySetResult(work());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
// If no thread is idle, all are blocked in COM calls — try to expand.
|
||||
if (Volatile.Read(ref _idleCount) == 0)
|
||||
{
|
||||
TryExpand();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
tcs.TrySetCanceled(cancellationToken);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
tcs.TrySetCanceled(CancellationToken.None);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// CompleteAdding was called — pool is shutting down.
|
||||
tcs.TrySetCanceled(CancellationToken.None);
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to spawn one additional thread, up to <c>maxThreads</c>.
|
||||
/// CAS on <c>_threadCount</c> ensures at most one thread wins per slot.
|
||||
/// </summary>
|
||||
private void TryExpand()
|
||||
{
|
||||
if (_disposed.Value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
var count = _threadCount;
|
||||
if (count >= _maxThreads)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _threadCount, count + 1, count) == count)
|
||||
{
|
||||
StartThread();
|
||||
return;
|
||||
}
|
||||
|
||||
// Another concurrent expand won this slot; recheck the ceiling.
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed.Set())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_workQueue.CompleteAdding();
|
||||
|
||||
// Give worker threads a chance to drain remaining items and exit.
|
||||
// After CompleteAdding, idle threads see IsCompleted and exit
|
||||
// quickly, but threads blocked in long COM calls won't return
|
||||
// until their call finishes — don't wait forever.
|
||||
var deadline = Environment.TickCount64 + DrainTimeoutMs;
|
||||
var spin = default(SpinWait);
|
||||
while (Volatile.Read(ref _threadCount) > 0 && Environment.TickCount64 < deadline)
|
||||
{
|
||||
spin.SpinOnce();
|
||||
}
|
||||
|
||||
// Dispose the queue even if threads are still alive. Threads
|
||||
// blocked in TryTake will get ObjectDisposedException and exit
|
||||
// via the catch in WorkerLoop. Threads busy in action!() will
|
||||
// finish their item, then hit ObjectDisposedException on the
|
||||
// next TryTake and exit.
|
||||
_workQueue.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
@@ -12,7 +13,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
public partial class DockBandSettingsViewModel : ObservableObject
|
||||
{
|
||||
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly DockBandSettings _dockSettingsModel;
|
||||
private readonly TopLevelViewModel _adapter;
|
||||
private readonly DockBandViewModel? _bandViewModel;
|
||||
@@ -128,19 +129,19 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
DockBandSettings dockSettingsModel,
|
||||
TopLevelViewModel topLevelAdapter,
|
||||
DockBandViewModel? bandViewModel,
|
||||
SettingsModel settingsModel)
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_dockSettingsModel = dockSettingsModel;
|
||||
_adapter = topLevelAdapter;
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsModel = settingsModel;
|
||||
_settingsService = settingsService;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
var inStart = dockSettings.StartBands.Any(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
if (inStart)
|
||||
{
|
||||
@@ -175,7 +176,7 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
_settingsService.Save();
|
||||
}
|
||||
|
||||
private void UpdatePinSide(DockPinSide value)
|
||||
@@ -188,7 +189,7 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
|
||||
public void SetBandPosition(DockPinSide side, int? index)
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
// Remove from all sides first
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
|
||||
@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -15,7 +16,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
public sealed partial class DockViewModel
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly DockPageContext _pageContext; // only to be used for our own context menu - not for dock bands themselves
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
@@ -29,18 +30,18 @@ public sealed partial class DockViewModel
|
||||
|
||||
public ObservableCollection<DockBandViewModel> EndItems { get; } = new();
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
|
||||
public IReadOnlyList<TopLevelViewModel> AllItems => _topLevelCommandManager.GetDockBandsSnapshot();
|
||||
|
||||
public DockViewModel(
|
||||
TopLevelCommandManager tlcManager,
|
||||
IContextMenuFactory contextMenuFactory,
|
||||
SettingsModel settings,
|
||||
TaskScheduler scheduler)
|
||||
TaskScheduler scheduler,
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
_settingsModel = settings;
|
||||
_settings = settings.DockSettings;
|
||||
_settingsService = settingsService;
|
||||
_settings = _settingsService.Settings.DockSettings;
|
||||
Scheduler = scheduler;
|
||||
_pageContext = new(this);
|
||||
|
||||
@@ -148,7 +149,7 @@ public sealed partial class DockViewModel
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
_settingsService.Save();
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
||||
@@ -193,7 +194,7 @@ public sealed partial class DockViewModel
|
||||
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
@@ -228,7 +229,7 @@ public sealed partial class DockViewModel
|
||||
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
@@ -301,7 +302,7 @@ public sealed partial class DockViewModel
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
_settingsService.Save();
|
||||
Logger.LogDebug("Saved band order to settings");
|
||||
}
|
||||
|
||||
@@ -316,7 +317,7 @@ public sealed partial class DockViewModel
|
||||
/// </summary>
|
||||
public void SnapshotBandOrder()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
_snapshotStartBands = dockSettings.StartBands.Select(b => b.Clone()).ToList();
|
||||
_snapshotCenterBands = dockSettings.CenterBands.Select(b => b.Clone()).ToList();
|
||||
_snapshotEndBands = dockSettings.EndBands.Select(b => b.Clone()).ToList();
|
||||
@@ -358,7 +359,7 @@ public sealed partial class DockViewModel
|
||||
band.RestoreShowLabels();
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
// Restore settings from snapshot
|
||||
dockSettings.StartBands.Clear();
|
||||
@@ -400,7 +401,7 @@ public sealed partial class DockViewModel
|
||||
return;
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
@@ -433,7 +434,7 @@ public sealed partial class DockViewModel
|
||||
|
||||
private void RebuildUICollections()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
// Create a lookup of all current band ViewModels
|
||||
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
|
||||
@@ -510,7 +511,7 @@ public sealed partial class DockViewModel
|
||||
|
||||
// Create settings for the new band
|
||||
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId, ShowLabels = null };
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
// Create the band view model
|
||||
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
|
||||
@@ -550,7 +551,7 @@ public sealed partial class DockViewModel
|
||||
public void UnpinBand(DockBandViewModel band)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
// Remove from settings
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
||||
@@ -616,7 +617,7 @@ public sealed partial class DockViewModel
|
||||
|
||||
private void EmitDockConfiguration()
|
||||
{
|
||||
var isDockEnabled = _settingsModel.EnableDock;
|
||||
var isDockEnabled = _settingsService.Settings.EnableDock;
|
||||
var dockSide = isDockEnabled ? _settings.Side.ToString().ToLowerInvariant() : "none";
|
||||
|
||||
static string FormatBands(List<DockBandSettings> bands) =>
|
||||
|
||||
@@ -23,8 +23,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
/// </summary>
|
||||
public sealed partial class DockAppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
@@ -37,18 +36,18 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_dockSettings.Theme;
|
||||
get => (int)_settingsService.Settings.DockSettings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _dockSettings.Theme;
|
||||
get => _settingsService.Settings.DockSettings.Theme;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Theme != value)
|
||||
if (_settingsService.Settings.DockSettings.Theme != value)
|
||||
{
|
||||
_dockSettings.Theme = value;
|
||||
_settingsService.Settings.DockSettings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
@@ -58,18 +57,18 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public int BackdropIndex
|
||||
{
|
||||
get => (int)_dockSettings.Backdrop;
|
||||
get => (int)_settingsService.Settings.DockSettings.Backdrop;
|
||||
set => Backdrop = (DockBackdrop)value;
|
||||
}
|
||||
|
||||
public DockBackdrop Backdrop
|
||||
{
|
||||
get => _dockSettings.Backdrop;
|
||||
get => _settingsService.Settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Backdrop != value)
|
||||
if (_settingsService.Settings.DockSettings.Backdrop != value)
|
||||
{
|
||||
_dockSettings.Backdrop = value;
|
||||
_settingsService.Settings.DockSettings.Backdrop = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackdropIndex));
|
||||
Save();
|
||||
@@ -79,12 +78,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _dockSettings.ColorizationMode;
|
||||
get => _settingsService.Settings.DockSettings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.ColorizationMode != value)
|
||||
if (_settingsService.Settings.DockSettings.ColorizationMode != value)
|
||||
{
|
||||
_dockSettings.ColorizationMode = value;
|
||||
_settingsService.Settings.DockSettings.ColorizationMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
@@ -107,18 +106,18 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_dockSettings.ColorizationMode;
|
||||
get => (int)_settingsService.Settings.DockSettings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _dockSettings.CustomThemeColor;
|
||||
get => _settingsService.Settings.DockSettings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.CustomThemeColor != value)
|
||||
if (_settingsService.Settings.DockSettings.CustomThemeColor != value)
|
||||
{
|
||||
_dockSettings.CustomThemeColor = value;
|
||||
_settingsService.Settings.DockSettings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
@@ -134,10 +133,10 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _dockSettings.CustomThemeColorIntensity;
|
||||
get => _settingsService.Settings.DockSettings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_dockSettings.CustomThemeColorIntensity = value;
|
||||
_settingsService.Settings.DockSettings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -145,12 +144,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _dockSettings.BackgroundImagePath ?? string.Empty;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImagePath != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImagePath != value)
|
||||
{
|
||||
_dockSettings.BackgroundImagePath = value;
|
||||
_settingsService.Settings.DockSettings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
@@ -165,12 +164,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _dockSettings.BackgroundImageOpacity;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageOpacity != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageOpacity = value;
|
||||
_settingsService.Settings.DockSettings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -179,12 +178,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBrightness;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBrightness != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBrightness = value;
|
||||
_settingsService.Settings.DockSettings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -193,12 +192,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBlurAmount;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBlurAmount != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBlurAmount = value;
|
||||
_settingsService.Settings.DockSettings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -207,12 +206,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _dockSettings.BackgroundImageFit;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageFit != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImageFit != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageFit = value;
|
||||
_settingsService.Settings.DockSettings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
@@ -237,15 +236,15 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
public bool IsCustomTintVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
public bool IsCustomTintIntensityVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsBackgroundControlsVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _dockSettings.ColorizationMode is ColorizationMode.None;
|
||||
public bool IsNoBackgroundVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
public bool IsAccentColorControlsVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
@@ -268,12 +267,11 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public DockAppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
public DockAppearanceSettingsViewModel(IThemeService themeService, ISettingsService settingsService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_dockSettings = settings.DockSettings;
|
||||
_settingsService = settingsService;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
@@ -281,7 +279,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _dockSettings.ColorizationMode != ColorizationMode.None;
|
||||
IsColorizationDetailsExpanded = _settingsService.Settings.DockSettings.ColorizationMode != ColorizationMode.None;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
@@ -302,7 +300,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_settingsService.Save();
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class FallbackSettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly FallbackSettings _fallbackSettings;
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
@@ -62,10 +63,10 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
public FallbackSettingsViewModel(
|
||||
TopLevelViewModel fallback,
|
||||
FallbackSettings fallbackSettings,
|
||||
SettingsModel settingsModel,
|
||||
ProviderSettingsViewModel providerSettings)
|
||||
ProviderSettingsViewModel providerSettings,
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_settings = settingsModel;
|
||||
_settingsService = settingsService;
|
||||
_fallbackSettings = fallbackSettings;
|
||||
|
||||
Id = fallback.Id;
|
||||
@@ -79,7 +80,7 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_settingsService.Save();
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
// 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.
|
||||
|
||||
/*
|
||||
#define CMDPAL_FF_MAINPAGE_TIME_FALLBACK_UPDATES
|
||||
*/
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Manages adaptive dispatch of fallback update work on a dedicated thread pool.
|
||||
/// Tracks per-command inflight calls, pending-retry slots, and enforces a per-batch
|
||||
/// sibling-spawn cap to prevent runaway thread expansion.
|
||||
/// </summary>
|
||||
internal sealed partial class FallbackUpdateManager : IDisposable
|
||||
{
|
||||
// For individual fallback item updates - if an item takes longer than this, we will detach it
|
||||
// and continue with others.
|
||||
private static readonly TimeSpan FallbackItemSlowTimeout = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_FALLBACK_UPDATES
|
||||
// For reporting only - if an item takes longer than this, we'll log it.
|
||||
private static readonly TimeSpan FallbackItemUltraSlowTimeout = TimeSpan.FromMilliseconds(1000);
|
||||
#endif
|
||||
|
||||
// Initial number of workers to use for fallback updates.
|
||||
private const int InitialFallbackWorkers = 2;
|
||||
|
||||
// Upper limit of threads in case things go awry
|
||||
private const int MaximumFallbackWorkersMaxThreads = 32;
|
||||
|
||||
// Per-command limit on concurrent in-flight COM calls. Prevents a single
|
||||
// misbehaving extension from monopolizing the pool across overlapping query batches.
|
||||
private const int MaxInflightPerFallback = 4;
|
||||
|
||||
// Per-batch cap on sibling workers
|
||||
private static readonly int MaxWorkersPerBatch = Math.Max(2, Environment.ProcessorCount / 2);
|
||||
|
||||
private readonly ConcurrentDictionary<string, InflightCounter> _inflightFallbacks = new();
|
||||
|
||||
// Dedicated background threads for fallback COM/RPC calls so they never block the
|
||||
// ThreadPool. Stuck extensions consume a dedicated thread, not a pool thread.
|
||||
// Max is intentionally above ProcessorCount: blocked threads consume no CPU, so
|
||||
// core count is not the right ceiling. Pool expands on demand and shrinks when idle.
|
||||
private readonly DedicatedThreadPool _fallbackThreadPool = new(minThreads: InitialFallbackWorkers, maxThreads: MaximumFallbackWorkersMaxThreads, name: "Fallbacks");
|
||||
|
||||
private readonly Action _onFallbackChanged;
|
||||
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_FALLBACK_UPDATES
|
||||
private ulong _updateBatchCounter;
|
||||
#endif
|
||||
|
||||
internal FallbackUpdateManager(Action onFallbackChanged)
|
||||
{
|
||||
_onFallbackChanged = onFallbackChanged;
|
||||
}
|
||||
|
||||
internal void BeginUpdate(string query, IReadOnlyList<TopLevelViewModel> commands, CancellationToken cancellationToken)
|
||||
{
|
||||
if (commands.Count == 0 || string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_FALLBACK_UPDATES
|
||||
var batchNumber = _updateBatchCounter++;
|
||||
Logger.LogDebug($"UpdateFallbacks: Batch start {batchNumber} for query '{query}'");
|
||||
#endif
|
||||
|
||||
// Adaptive dispatch on dedicated threads — same semantics as the old
|
||||
// ParallelHelper.AdaptiveForEachAdaptiveAsync, but without any ThreadPool involvement:
|
||||
// - Start 2 workers; each claims commands via a shared atomic index (FIFO, no double-work).
|
||||
// - If a command is slow (> FallbackItemSlowTimeout), the worker spawns a sibling so
|
||||
// remaining fast commands aren't blocked waiting in the worker's loop.
|
||||
// - _onFallbackChanged is called on the dedicated thread when a result changes
|
||||
var sharedIndex = 0;
|
||||
var totalCommands = commands.Count;
|
||||
var startingWorkers = Math.Min(InitialFallbackWorkers, totalCommands);
|
||||
var activeWorkerCount = startingWorkers;
|
||||
|
||||
void Worker()
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var i = Interlocked.Increment(ref sharedIndex) - 1;
|
||||
if (i >= totalCommands)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var command = commands[i];
|
||||
var counter = _inflightFallbacks.GetOrAdd(command.Id, static _ => new InflightCounter());
|
||||
if (!counter.TryClaim(MaxInflightPerFallback))
|
||||
{
|
||||
// At capacity — store this query as a pending retry so it runs
|
||||
// when one of the in-flight calls finishes. Latest query wins.
|
||||
var pendingCommand = command;
|
||||
var pendingQuery = query;
|
||||
var pendingCt = cancellationToken;
|
||||
counter.SetPending(() => RetryFallbackUpdate(pendingCommand, pendingQuery, pendingCt, counter), pendingCt);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Arm a timer: if this item is still running after FallbackItemSlowTimeout,
|
||||
// spawn a sibling worker WHILE we're blocked in the COM call so remaining
|
||||
// commands don't have to wait for us to finish first.
|
||||
// Linking to cancellationToken cancels the timer immediately when the outer
|
||||
// query is abandoned — preventing stale siblings from being scheduled.
|
||||
// Disposing the linked CTS at iteration end removes the link registration.
|
||||
using var expandCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
expandCts.CancelAfter(FallbackItemSlowTimeout);
|
||||
expandCts.Token.Register(() =>
|
||||
{
|
||||
// Fires on timeout (slow item) OR on outer cancellation.
|
||||
// Only spawn a sibling on timeout — when the outer query is still active.
|
||||
if (!cancellationToken.IsCancellationRequested && Volatile.Read(ref sharedIndex) < totalCommands)
|
||||
{
|
||||
// Per-batch cap — restore the constraint from ParallelHelper
|
||||
var current = Volatile.Read(ref activeWorkerCount);
|
||||
if (current < MaxWorkersPerBatch
|
||||
&& Interlocked.CompareExchange(ref activeWorkerCount, current + 1, current) == current)
|
||||
{
|
||||
_ = _fallbackThreadPool.QueueAsync(Worker, cancellationToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var changed = false;
|
||||
try
|
||||
{
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_FALLBACK_UPDATES
|
||||
var sw = Stopwatch.StartNew();
|
||||
Logger.LogDebug($"UpdateFallbacks: Worker: command id '{command.Id}', '{command.DisplayTitle}' updating with '{query}'");
|
||||
#endif
|
||||
changed = command.SafeUpdateFallbackTextSynchronous(query);
|
||||
#if CMDPAL_FF_MAINPAGE_TIME_FALLBACK_UPDATES
|
||||
var elapsed = sw.Elapsed;
|
||||
var tail = elapsed > FallbackItemSlowTimeout ? " is slow" : string.Empty;
|
||||
if (elapsed > FallbackItemUltraSlowTimeout)
|
||||
{
|
||||
tail += " <---------------- (ultra slow)";
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"UpdateFallbacks: Worker: command id '{command.Id}', '{command.DisplayTitle}' updated with '{query}' processed in {elapsed}, has {(changed ? "changed" : "not changed")} and title is '{command.Title}'{tail}");
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"UpdateFallbacks: Worker: command id '{command.Id}', '{command.DisplayTitle}' failed to update fallback text with '{query}'", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
counter.Release();
|
||||
DispatchPending(counter.TakePending());
|
||||
}
|
||||
|
||||
// Guard against a stale refresh if the COM call returned after cancellation.
|
||||
if (changed && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_onFallbackChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatches a pending work item to the dedicated pool. The pending's
|
||||
// own CT is forwarded so the pool can skip it at dequeue time when the
|
||||
// originating query batch has been superseded by a newer keystroke.
|
||||
void DispatchPending(PendingWork? pending)
|
||||
{
|
||||
if (pending == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = _fallbackThreadPool.QueueAsync(pending.Work, pending.CancellationToken);
|
||||
}
|
||||
|
||||
for (var i = 0; i < startingWorkers; i++)
|
||||
{
|
||||
_ = _fallbackThreadPool.QueueAsync(Worker, cancellationToken);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
// One-shot retry for a command that was skipped due to MaxInflightPerFallback.
|
||||
// Claims a slot, runs the COM call, releases, and propagates the next pending (if any).
|
||||
void RetryFallbackUpdate(TopLevelViewModel cmd, string q, CancellationToken ct, InflightCounter ctr)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctr.TryClaim(MaxInflightPerFallback))
|
||||
{
|
||||
// Still at capacity (a newer worker claimed the freed slot first).
|
||||
// The pending was already consumed from TakePending, so it's dropped here.
|
||||
return;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
try
|
||||
{
|
||||
changed = cmd.SafeUpdateFallbackTextSynchronous(q);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"UpdateFallbacks: Pending retry: command id '{cmd.Id}', '{cmd.DisplayTitle}' failed with '{q}'", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ctr.Release();
|
||||
DispatchPending(ctr.TakePending());
|
||||
}
|
||||
|
||||
if (changed && !ct.IsCancellationRequested)
|
||||
{
|
||||
_onFallbackChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fallbackThreadPool.Dispose();
|
||||
_inflightFallbacks.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pending work item paired with the cancellation token of the query
|
||||
/// batch that created it, so the pool can skip it at dequeue time when
|
||||
/// a newer keystroke has already superseded the query.
|
||||
/// </summary>
|
||||
private sealed record PendingWork(Action Work, CancellationToken CancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe counter for tracking concurrent in-flight calls per command,
|
||||
/// with a single pending retry slot for queries that couldn't claim immediately.
|
||||
/// </summary>
|
||||
private sealed class InflightCounter
|
||||
{
|
||||
private int _count;
|
||||
|
||||
// Latest pending work item. Only one is stored; newer queries overwrite older ones.
|
||||
private PendingWork? _pendingWork;
|
||||
|
||||
/// <summary>
|
||||
/// Try to claim a slot. Returns true if the count was below
|
||||
/// <paramref name="max"/> and was incremented; false if at capacity.
|
||||
/// </summary>
|
||||
public bool TryClaim(int max)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var current = Volatile.Read(ref _count);
|
||||
if (current >= max)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _count, current + 1, current) == current)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a pending work item to run when the next slot opens.
|
||||
/// Overwrites any previously stored item — latest query always wins.
|
||||
/// </summary>
|
||||
public void SetPending(Action work, CancellationToken ct) => Interlocked.Exchange(ref _pendingWork, new PendingWork(work, ct));
|
||||
|
||||
/// <summary>
|
||||
/// Atomically removes and returns any pending work item, or null if none.
|
||||
/// </summary>
|
||||
public PendingWork? TakePending() => Interlocked.Exchange(ref _pendingWork, null);
|
||||
|
||||
public void Release() => Interlocked.Decrement(ref _count);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -12,10 +13,10 @@ public partial class HotkeyManager : ObservableObject
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly List<TopLevelHotkey> _commandHotkeys;
|
||||
|
||||
public HotkeyManager(TopLevelCommandManager tlcManager, SettingsModel settings)
|
||||
public HotkeyManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_commandHotkeys = settings.CommandHotkeys;
|
||||
_commandHotkeys = settingsService.Settings.CommandHotkeys;
|
||||
}
|
||||
|
||||
public void UpdateHotkey(string commandId, HotkeySettings? hotkey)
|
||||
|
||||
@@ -22,7 +22,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo
|
||||
|
||||
public IconDataViewModel Dark { get; private set; }
|
||||
|
||||
public IconDataViewModel IconForTheme(bool light) => Light = light ? Light : Dark;
|
||||
public IconDataViewModel IconForTheme(bool light) => light ? Light : Dark;
|
||||
|
||||
public bool HasIcon(bool light) => IconForTheme(light).HasIcon;
|
||||
|
||||
|
||||
@@ -159,7 +159,6 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
UpdateShowDetailsCommand();
|
||||
break;
|
||||
case nameof(model.MoreCommands):
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
AddShowDetailsCommands();
|
||||
break;
|
||||
case nameof(model.Title):
|
||||
@@ -195,19 +194,27 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
pageContext is ListViewModel listViewModel &&
|
||||
!listViewModel.ShowDetails)
|
||||
{
|
||||
// Check if "Show Details" action already exists to prevent duplicates
|
||||
if (!MoreCommands.Any(cmd => cmd is CommandContextItemViewModel contextItemViewModel &&
|
||||
contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId))
|
||||
var addedCommand = false;
|
||||
lock (MoreCommandsLock)
|
||||
{
|
||||
// Create the view model for the show details command
|
||||
var showDetailsCommand = new ShowDetailsCommand(Details);
|
||||
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
|
||||
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||
// Check if "Show Details" action already exists to prevent duplicates
|
||||
if (!UnsafeMoreCommands.Any(cmd => cmd is CommandContextItemViewModel contextItemViewModel &&
|
||||
contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId))
|
||||
{
|
||||
var showDetailsCommand = new ShowDetailsCommand(Details);
|
||||
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
|
||||
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
UnsafeMoreCommands.Add(showDetailsContextItemViewModel);
|
||||
RefreshMoreCommandStateUnsafe();
|
||||
addedCommand = true;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
if (addedCommand)
|
||||
{
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,22 +229,27 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
pageContext is ListViewModel listViewModel &&
|
||||
!listViewModel.ShowDetails)
|
||||
{
|
||||
var existingCommand = MoreCommands.FirstOrDefault(cmd =>
|
||||
cmd is CommandContextItemViewModel contextItemViewModel &&
|
||||
contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId);
|
||||
|
||||
// If the command already exists, remove it to update with the new details
|
||||
if (existingCommand is not null)
|
||||
CommandContextItemViewModel? oldCommand = null;
|
||||
lock (MoreCommandsLock)
|
||||
{
|
||||
MoreCommands.Remove(existingCommand);
|
||||
oldCommand = UnsafeMoreCommands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.FirstOrDefault(contextItemViewModel => contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId);
|
||||
|
||||
if (oldCommand is not null)
|
||||
{
|
||||
UnsafeMoreCommands.Remove(oldCommand);
|
||||
}
|
||||
|
||||
var showDetailsCommand = new ShowDetailsCommand(Details);
|
||||
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
|
||||
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
UnsafeMoreCommands.Add(showDetailsContextItemViewModel);
|
||||
RefreshMoreCommandStateUnsafe();
|
||||
}
|
||||
|
||||
// Create the view model for the show details command
|
||||
var showDetailsCommand = new ShowDetailsCommand(Details);
|
||||
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
|
||||
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
|
||||
showDetailsContextItemViewModel.SlowInitializeProperties();
|
||||
MoreCommands.Add(showDetailsContextItemViewModel);
|
||||
oldCommand?.SafeCleanup();
|
||||
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
public bool IsMainPage { get; init; }
|
||||
|
||||
public bool HasCustomDebounceLogic => IsMainPage;
|
||||
|
||||
private bool _isDynamic;
|
||||
|
||||
private Task? _initializeItemsTask;
|
||||
|
||||
@@ -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 GoToPageResultMessage(Microsoft.CommandPalette.Extensions.IGoToPageArgs? Args);
|
||||
@@ -18,11 +18,11 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
|
||||
|
||||
public interface IContextMenuContext : INotifyPropertyChanged
|
||||
{
|
||||
public IEnumerable<IContextItemViewModel> MoreCommands { get; }
|
||||
public IReadOnlyList<IContextItemViewModel> MoreCommands { get; }
|
||||
|
||||
public bool HasMoreCommands { get; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands { get; }
|
||||
public IReadOnlyList<IContextItemViewModel> AllCommands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mapping of key -> command item for this particular item's
|
||||
|
||||
@@ -484,7 +484,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit dock.
|
||||
/// Looks up a localized string similar to Edit Dock.
|
||||
/// </summary>
|
||||
public static string dock_edit_dock_name {
|
||||
get {
|
||||
@@ -511,7 +511,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Dock settings.
|
||||
/// Looks up a localized string similar to Settings.
|
||||
/// </summary>
|
||||
public static string dock_settings_name {
|
||||
get {
|
||||
|
||||
@@ -277,11 +277,11 @@
|
||||
<value>Fallbacks</value>
|
||||
</data>
|
||||
<data name="dock_edit_dock_name" xml:space="preserve">
|
||||
<value>Edit dock</value>
|
||||
<value>Edit Dock</value>
|
||||
<comment>Command name for editing the dock</comment>
|
||||
</data>
|
||||
<data name="dock_settings_name" xml:space="preserve">
|
||||
<value>Dock settings</value>
|
||||
<value>Settings</value>
|
||||
<comment>Command name for opening dock settings</comment>
|
||||
</data>
|
||||
<data name="ShowDetailsCommand" xml:space="preserve">
|
||||
|
||||
@@ -12,6 +12,7 @@ public class ProviderSettings
|
||||
private readonly string[] _excludedBuiltInFallbacks = [
|
||||
"com.microsoft.cmdpal.builtin.indexer.fallback",
|
||||
"com.microsoft.cmdpal.builtin.calculator.fallback",
|
||||
"com.microsoft.cmdpal.builtin.remotedesktop.fallback",
|
||||
];
|
||||
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -10,6 +10,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
@@ -22,7 +23,7 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
||||
|
||||
private readonly CommandProviderWrapper _provider;
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly Lock _initializeSettingsLock = new();
|
||||
|
||||
private Task? _initializeSettingsTask;
|
||||
@@ -30,11 +31,11 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
||||
public ProviderSettingsViewModel(
|
||||
CommandProviderWrapper provider,
|
||||
ProviderSettings providerSettings,
|
||||
SettingsModel settings)
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_provider = provider;
|
||||
_providerSettings = providerSettings;
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
|
||||
LoadingSettings = _provider.Settings?.HasSettings ?? false;
|
||||
|
||||
@@ -179,18 +180,18 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
||||
{
|
||||
if (_providerSettings.FallbackCommands.TryGetValue(fallbackItem.Id, out var fallbackSettings))
|
||||
{
|
||||
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, fallbackSettings, _settings, this));
|
||||
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, fallbackSettings, this, _settingsService));
|
||||
}
|
||||
else
|
||||
{
|
||||
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, new(), _settings, this));
|
||||
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, new(), this, _settingsService));
|
||||
}
|
||||
}
|
||||
|
||||
FallbackCommands = fallbackViewModels;
|
||||
}
|
||||
|
||||
private void Save() => SettingsModel.SaveSettings(_settings);
|
||||
private void Save() => _settingsService.Save();
|
||||
|
||||
private void InitializeSettingsPage()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IAppStateService"/>.
|
||||
/// Handles loading, saving, and change notification for <see cref="AppStateModel"/>.
|
||||
/// </summary>
|
||||
public sealed class AppStateService : IAppStateService
|
||||
{
|
||||
private readonly IPersistenceService _persistence;
|
||||
private readonly IApplicationInfoService _appInfoService;
|
||||
private readonly string _filePath;
|
||||
|
||||
public AppStateService(IPersistenceService persistence, IApplicationInfoService appInfoService)
|
||||
{
|
||||
_persistence = persistence;
|
||||
_appInfoService = appInfoService;
|
||||
_filePath = StateJsonPath();
|
||||
State = _persistence.Load(_filePath, JsonSerializationContext.Default.AppStateModel);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AppStateModel State { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event TypedEventHandler<IAppStateService, AppStateModel>? StateChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Save()
|
||||
{
|
||||
_persistence.Save(State, _filePath, JsonSerializationContext.Default.AppStateModel);
|
||||
StateChanged?.Invoke(this, State);
|
||||
}
|
||||
|
||||
private string StateJsonPath()
|
||||
{
|
||||
var directory = _appInfoService.ConfigDirectory;
|
||||
return Path.Combine(directory, "state.json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the lifecycle of <see cref="AppStateModel"/>: load, save, and change notification.
|
||||
/// </summary>
|
||||
public interface IAppStateService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current application state instance.
|
||||
/// </summary>
|
||||
AppStateModel State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Persists the current state to disk and raises <see cref="StateChanged"/>.
|
||||
/// </summary>
|
||||
void Save();
|
||||
|
||||
/// <summary>
|
||||
/// Raised after state has been saved to disk.
|
||||
/// </summary>
|
||||
event TypedEventHandler<IAppStateService, AppStateModel> StateChanged;
|
||||
}
|
||||
@@ -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 System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides AOT-compatible JSON file persistence with shallow-merge strategy.
|
||||
/// </summary>
|
||||
public interface IPersistenceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads and deserializes a model from the specified JSON file.
|
||||
/// Returns a new <typeparamref name="T"/> instance when the file is missing or unreadable.
|
||||
/// </summary>
|
||||
T Load<T>(string filePath, JsonTypeInfo<T> typeInfo)
|
||||
where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// Serializes <paramref name="model"/>, shallow-merges into the existing file
|
||||
/// (preserving unknown keys), and writes the result back to disk.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to persist.</param>
|
||||
/// <param name="filePath">Target JSON file path.</param>
|
||||
/// <param name="typeInfo">AOT-compatible type metadata.</param>
|
||||
void Save<T>(T model, string filePath, JsonTypeInfo<T> typeInfo);
|
||||
}
|
||||
@@ -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 Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the lifecycle of <see cref="SettingsModel"/>: load, save, migration, and change notification.
|
||||
/// </summary>
|
||||
public interface ISettingsService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current settings instance.
|
||||
/// </summary>
|
||||
SettingsModel Settings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Persists the current settings to disk.
|
||||
/// </summary>
|
||||
/// <param name="hotReload">When <see langword="true"/>, raises <see cref="SettingsChanged"/> after saving.</param>
|
||||
void Save(bool hotReload = true);
|
||||
|
||||
/// <summary>
|
||||
/// Raised after settings are saved with <paramref name="hotReload"/> enabled, or after <see cref="Reload"/>.
|
||||
/// </summary>
|
||||
event TypedEventHandler<ISettingsService, SettingsModel> SettingsChanged;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IPersistenceService"/> that reads/writes
|
||||
/// JSON files with a shallow-merge strategy to preserve unknown keys.
|
||||
/// </summary>
|
||||
public sealed class PersistenceService : IPersistenceService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public T Load<T>(string filePath, JsonTypeInfo<T> typeInfo)
|
||||
where T : new()
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Logger.LogDebug("Settings file not found at {FilePath}", filePath);
|
||||
return new T();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jsonContent = File.ReadAllText(filePath);
|
||||
var loaded = JsonSerializer.Deserialize(jsonContent, typeInfo);
|
||||
|
||||
if (loaded is null)
|
||||
{
|
||||
Logger.LogDebug("Failed to parse settings file at {FilePath}", filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Successfully loaded settings file from {FilePath}", filePath);
|
||||
}
|
||||
|
||||
return loaded ?? new T();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load settings from {filePath}:", ex);
|
||||
}
|
||||
|
||||
return new T();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Save<T>(T model, string filePath, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsJson = JsonSerializer.Serialize(model, typeInfo);
|
||||
|
||||
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
|
||||
{
|
||||
Logger.LogError("Failed to parse serialized model as JsonObject.");
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var serialized = newSettings.ToJsonString(typeInfo.Options);
|
||||
File.WriteAllText(filePath, serialized);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save to {filePath}:", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// 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 System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="ISettingsService"/>.
|
||||
/// Handles loading, saving, migration, and change notification for <see cref="SettingsModel"/>.
|
||||
/// </summary>
|
||||
public sealed class SettingsService : ISettingsService
|
||||
{
|
||||
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
|
||||
|
||||
private readonly IPersistenceService _persistence;
|
||||
private readonly IApplicationInfoService _appInfoService;
|
||||
private readonly string _filePath;
|
||||
|
||||
public SettingsService(IPersistenceService persistence, IApplicationInfoService appInfoService)
|
||||
{
|
||||
_persistence = persistence;
|
||||
_appInfoService = appInfoService;
|
||||
_filePath = SettingsJsonPath();
|
||||
Settings = _persistence.Load(_filePath, JsonSerializationContext.Default.SettingsModel);
|
||||
ApplyMigrations();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SettingsModel Settings { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event TypedEventHandler<ISettingsService, SettingsModel>? SettingsChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Save(bool hotReload = true)
|
||||
{
|
||||
_persistence.Save(
|
||||
Settings,
|
||||
_filePath,
|
||||
JsonSerializationContext.Default.SettingsModel);
|
||||
|
||||
if (hotReload)
|
||||
{
|
||||
SettingsChanged?.Invoke(this, Settings);
|
||||
}
|
||||
}
|
||||
|
||||
private string SettingsJsonPath()
|
||||
{
|
||||
var directory = _appInfoService.ConfigDirectory;
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
private void ApplyMigrations()
|
||||
{
|
||||
var migratedAny = false;
|
||||
|
||||
try
|
||||
{
|
||||
var jsonContent = File.Exists(_filePath) ? File.ReadAllText(_filePath) : null;
|
||||
if (jsonContent is not null && JsonNode.Parse(jsonContent) is JsonObject root)
|
||||
{
|
||||
migratedAny |= TryMigrate(
|
||||
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
|
||||
root,
|
||||
Settings,
|
||||
nameof(SettingsModel.AutoGoHomeInterval),
|
||||
DeprecatedHotkeyGoesHomeKey,
|
||||
(model, goesHome) => model.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
|
||||
JsonSerializationContext.Default.Boolean);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Migration check failed: {ex}");
|
||||
}
|
||||
|
||||
if (migratedAny)
|
||||
{
|
||||
Save(hotReload: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMigrate<T>(
|
||||
string migrationName,
|
||||
JsonObject root,
|
||||
SettingsModel model,
|
||||
string newKey,
|
||||
string oldKey,
|
||||
Action<SettingsModel, T> apply,
|
||||
JsonTypeInfo<T> jsonTypeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (root.ContainsKey(newKey) && root[newKey] is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
|
||||
{
|
||||
var value = oldNode.Deserialize(jsonTypeInfo);
|
||||
apply(model, value!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error during migration {migrationName}.", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.UI;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
@@ -29,7 +28,7 @@ public class DockSettings
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
public Color CustomThemeColor { get; set; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent and COM in class init
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
|
||||
|
||||
@@ -2,30 +2,15 @@
|
||||
// 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 System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class SettingsModel : ObservableObject
|
||||
{
|
||||
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
|
||||
|
||||
[JsonIgnore]
|
||||
public static readonly string FilePath;
|
||||
|
||||
public event TypedEventHandler<SettingsModel, object?>? SettingsChanged;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// SETTINGS HERE
|
||||
public static HotkeySettings DefaultActivationShortcut { get; } = new HotkeySettings(true, false, true, false, 0x20); // win+alt+space
|
||||
@@ -77,7 +62,7 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
public Color CustomThemeColor { get; set; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
|
||||
@@ -102,11 +87,6 @@ public partial class SettingsModel : ObservableObject
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static SettingsModel()
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
}
|
||||
|
||||
public ProviderSettings GetProviderSettings(CommandProviderWrapper provider)
|
||||
{
|
||||
ProviderSettings? settings;
|
||||
@@ -143,165 +123,6 @@ public partial class SettingsModel : ObservableObject
|
||||
return globalFallbacks.ToArray();
|
||||
}
|
||||
|
||||
public static SettingsModel LoadSettings()
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(SettingsModel.FilePath)} before calling {nameof(LoadSettings)}");
|
||||
}
|
||||
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
Debug.WriteLine("The provided settings file does not exist");
|
||||
return new();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel) ?? new();
|
||||
|
||||
var migratedAny = false;
|
||||
try
|
||||
{
|
||||
if (JsonNode.Parse(jsonContent) is JsonObject root)
|
||||
{
|
||||
migratedAny |= ApplyMigrations(root, loaded);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Migration check failed: {ex}");
|
||||
}
|
||||
|
||||
Debug.WriteLine("Loaded settings file");
|
||||
|
||||
if (migratedAny)
|
||||
{
|
||||
SaveSettings(loaded);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.ToString());
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
private static bool ApplyMigrations(JsonObject root, SettingsModel model)
|
||||
{
|
||||
var migrated = false;
|
||||
|
||||
// Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)
|
||||
// The old 'HotkeyGoesHome' boolean indicated whether the "go home" action should happen immediately (true) or never (false).
|
||||
// The new 'AutoGoHomeInterval' uses a TimeSpan: 'TimeSpan.Zero' means immediate, 'Timeout.InfiniteTimeSpan' means never.
|
||||
migrated |= TryMigrate(
|
||||
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
|
||||
root,
|
||||
model,
|
||||
nameof(AutoGoHomeInterval),
|
||||
DeprecatedHotkeyGoesHomeKey,
|
||||
(settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
|
||||
JsonSerializationContext.Default.Boolean);
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
private static bool TryMigrate<T>(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action<SettingsModel, T> apply, JsonTypeInfo<T> jsonTypeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If new key already present, skip migration
|
||||
if (root.ContainsKey(newKey) && root[newKey] is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If old key present, try to deserialize and apply
|
||||
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
|
||||
{
|
||||
var value = oldNode.Deserialize<T>(jsonTypeInfo);
|
||||
apply(model, value!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error during migration {migrationName}.", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void SaveSettings(SettingsModel model, bool hotReload = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveSettings)}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Serialize the main dictionary to JSON and save it to the file
|
||||
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.SettingsModel);
|
||||
|
||||
// Is it valid JSON?
|
||||
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
|
||||
{
|
||||
// Now, read the existing content from the file
|
||||
var oldContent = File.Exists(FilePath) ? File.ReadAllText(FilePath) : "{}";
|
||||
|
||||
// Is it valid JSON?
|
||||
if (JsonNode.Parse(oldContent) is JsonObject savedSettings)
|
||||
{
|
||||
foreach (var item in newSettings)
|
||||
{
|
||||
savedSettings[item.Key] = item.Value?.DeepClone();
|
||||
}
|
||||
|
||||
// Remove deprecated keys
|
||||
savedSettings.Remove(DeprecatedHotkeyGoesHomeKey);
|
||||
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
// TODO: Instead of just raising the event here, we should
|
||||
// have a file change watcher on the settings file, and
|
||||
// reload the settings then
|
||||
if (hotReload)
|
||||
{
|
||||
model.SettingsChanged?.Invoke(model, null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed to parse settings file as JsonObject.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed to parse settings file as JsonObject.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the settings is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
|
||||
// private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||
// {
|
||||
|
||||
@@ -27,7 +27,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
TimeSpan.FromSeconds(180),
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
@@ -38,10 +38,10 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
get => _settingsService.Settings.Hotkey;
|
||||
set
|
||||
{
|
||||
_settings.Hotkey = value ?? SettingsModel.DefaultActivationShortcut;
|
||||
_settingsService.Settings.Hotkey = value ?? SettingsModel.DefaultActivationShortcut;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
|
||||
Save();
|
||||
}
|
||||
@@ -49,10 +49,10 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public bool UseLowLevelGlobalHotkey
|
||||
{
|
||||
get => _settings.UseLowLevelGlobalHotkey;
|
||||
get => _settingsService.Settings.UseLowLevelGlobalHotkey;
|
||||
set
|
||||
{
|
||||
_settings.UseLowLevelGlobalHotkey = value;
|
||||
_settingsService.Settings.UseLowLevelGlobalHotkey = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
|
||||
Save();
|
||||
}
|
||||
@@ -60,100 +60,100 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public bool AllowExternalReload
|
||||
{
|
||||
get => _settings.AllowExternalReload;
|
||||
get => _settingsService.Settings.AllowExternalReload;
|
||||
set
|
||||
{
|
||||
_settings.AllowExternalReload = value;
|
||||
_settingsService.Settings.AllowExternalReload = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAppDetails
|
||||
{
|
||||
get => _settings.ShowAppDetails;
|
||||
get => _settingsService.Settings.ShowAppDetails;
|
||||
set
|
||||
{
|
||||
_settings.ShowAppDetails = value;
|
||||
_settingsService.Settings.ShowAppDetails = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool BackspaceGoesBack
|
||||
{
|
||||
get => _settings.BackspaceGoesBack;
|
||||
get => _settingsService.Settings.BackspaceGoesBack;
|
||||
set
|
||||
{
|
||||
_settings.BackspaceGoesBack = value;
|
||||
_settingsService.Settings.BackspaceGoesBack = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SingleClickActivates
|
||||
{
|
||||
get => _settings.SingleClickActivates;
|
||||
get => _settingsService.Settings.SingleClickActivates;
|
||||
set
|
||||
{
|
||||
_settings.SingleClickActivates = value;
|
||||
_settingsService.Settings.SingleClickActivates = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HighlightSearchOnActivate
|
||||
{
|
||||
get => _settings.HighlightSearchOnActivate;
|
||||
get => _settingsService.Settings.HighlightSearchOnActivate;
|
||||
set
|
||||
{
|
||||
_settings.HighlightSearchOnActivate = value;
|
||||
_settingsService.Settings.HighlightSearchOnActivate = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool KeepPreviousQuery
|
||||
{
|
||||
get => _settings.KeepPreviousQuery;
|
||||
get => _settingsService.Settings.KeepPreviousQuery;
|
||||
set
|
||||
{
|
||||
_settings.KeepPreviousQuery = value;
|
||||
_settingsService.Settings.KeepPreviousQuery = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public int MonitorPositionIndex
|
||||
{
|
||||
get => (int)_settings.SummonOn;
|
||||
get => (int)_settingsService.Settings.SummonOn;
|
||||
set
|
||||
{
|
||||
_settings.SummonOn = (MonitorBehavior)value;
|
||||
_settingsService.Settings.SummonOn = (MonitorBehavior)value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSystemTrayIcon
|
||||
{
|
||||
get => _settings.ShowSystemTrayIcon;
|
||||
get => _settingsService.Settings.ShowSystemTrayIcon;
|
||||
set
|
||||
{
|
||||
_settings.ShowSystemTrayIcon = value;
|
||||
_settingsService.Settings.ShowSystemTrayIcon = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnoreShortcutWhenFullscreen
|
||||
{
|
||||
get => _settings.IgnoreShortcutWhenFullscreen;
|
||||
get => _settingsService.Settings.IgnoreShortcutWhenFullscreen;
|
||||
set
|
||||
{
|
||||
_settings.IgnoreShortcutWhenFullscreen = value;
|
||||
_settingsService.Settings.IgnoreShortcutWhenFullscreen = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool DisableAnimations
|
||||
{
|
||||
get => _settings.DisableAnimations;
|
||||
get => _settingsService.Settings.DisableAnimations;
|
||||
set
|
||||
{
|
||||
_settings.DisableAnimations = value;
|
||||
_settingsService.Settings.DisableAnimations = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval);
|
||||
var index = AutoGoHomeIntervals.IndexOf(_settingsService.Settings.AutoGoHomeInterval);
|
||||
return index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
if (value >= 0 && value < AutoGoHomeIntervals.Count)
|
||||
{
|
||||
_settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
|
||||
_settingsService.Settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
|
||||
}
|
||||
|
||||
Save();
|
||||
@@ -179,60 +179,60 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public int EscapeKeyBehaviorIndex
|
||||
{
|
||||
get => (int)_settings.EscapeKeyBehaviorSetting;
|
||||
get => (int)_settingsService.Settings.EscapeKeyBehaviorSetting;
|
||||
set
|
||||
{
|
||||
_settings.EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value;
|
||||
_settingsService.Settings.EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockSide Dock_Side
|
||||
{
|
||||
get => _settings.DockSettings.Side;
|
||||
get => _settingsService.Settings.DockSettings.Side;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Side = value;
|
||||
_settingsService.Settings.DockSettings.Side = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockSize Dock_DockSize
|
||||
{
|
||||
get => _settings.DockSettings.DockSize;
|
||||
get => _settingsService.Settings.DockSettings.DockSize;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.DockSize = value;
|
||||
_settingsService.Settings.DockSettings.DockSize = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockBackdrop Dock_Backdrop
|
||||
{
|
||||
get => _settings.DockSettings.Backdrop;
|
||||
get => _settingsService.Settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Backdrop = value;
|
||||
_settingsService.Settings.DockSettings.Backdrop = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dock_ShowLabels
|
||||
{
|
||||
get => _settings.DockSettings.ShowLabels;
|
||||
get => _settingsService.Settings.DockSettings.ShowLabels;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.ShowLabels = value;
|
||||
_settingsService.Settings.DockSettings.ShowLabels = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableDock
|
||||
{
|
||||
get => _settings.EnableDock;
|
||||
get => _settingsService.Settings.EnableDock;
|
||||
set
|
||||
{
|
||||
_settings.EnableDock = value;
|
||||
_settingsService.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
|
||||
@@ -245,26 +245,26 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public SettingsExtensionsViewModel Extensions { get; }
|
||||
|
||||
public SettingsViewModel(SettingsModel settings, TopLevelCommandManager topLevelCommandManager, TaskScheduler scheduler, IThemeService themeService)
|
||||
public SettingsViewModel(TopLevelCommandManager topLevelCommandManager, TaskScheduler scheduler, IThemeService themeService, ISettingsService settingsService)
|
||||
{
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
|
||||
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, settingsService);
|
||||
DockAppearance = new DockAppearanceSettingsViewModel(themeService, settingsService);
|
||||
|
||||
var activeProviders = GetCommandProviders();
|
||||
var allProviderSettings = _settings.ProviderSettings;
|
||||
var allProviderSettings = _settingsService.Settings.ProviderSettings;
|
||||
|
||||
var fallbacks = new List<FallbackSettingsViewModel>();
|
||||
var currentRankings = _settings.FallbackRanks;
|
||||
var currentRankings = _settingsService.Settings.FallbackRanks;
|
||||
var needsSave = false;
|
||||
|
||||
foreach (var item in activeProviders)
|
||||
{
|
||||
var providerSettings = settings.GetProviderSettings(item);
|
||||
var providerSettings = _settingsService.Settings.GetProviderSettings(item);
|
||||
|
||||
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _settings);
|
||||
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, settingsService);
|
||||
CommandProviders.Add(settingsModel);
|
||||
|
||||
fallbacks.AddRange(settingsModel.FallbackCommands);
|
||||
@@ -306,10 +306,10 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public void ApplyFallbackSort()
|
||||
{
|
||||
_settings.FallbackRanks = FallbackRankings.Select(s => s.Id).ToArray();
|
||||
_settingsService.Settings.FallbackRanks = FallbackRankings.Select(s => s.Id).ToArray();
|
||||
Save();
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackRankings)));
|
||||
}
|
||||
|
||||
private void Save() => SettingsModel.SaveSettings(_settings);
|
||||
private void Save() => _settingsService.Save();
|
||||
}
|
||||
|
||||
@@ -467,6 +467,16 @@ public partial class ShellViewModel : ObservableObject,
|
||||
UnsafeHandleCommandResult(a.Result);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandResultKind.GoToPage:
|
||||
{
|
||||
if (result.Args is IGoToPageArgs a)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<GoToPageResultMessage>(new(a));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,9 +585,9 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
|
||||
// Then find all the top-level commands that belonged to that extension
|
||||
List<TopLevelViewModel> commandsToRemove = [];
|
||||
List<TopLevelViewModel> bandsToRemove = [];
|
||||
lock (TopLevelCommands)
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
foreach (var extension in extensions)
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var command in TopLevelCommands)
|
||||
{
|
||||
@@ -597,7 +597,10 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
|
||||
commandsToRemove.Add(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
foreach (var band in DockBands)
|
||||
{
|
||||
var host = band.ExtensionHost;
|
||||
@@ -675,6 +678,14 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<TopLevelViewModel> GetDockBandsSnapshot()
|
||||
{
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
return [.. DockBands];
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ReloadCommandsMessage message) =>
|
||||
_ = ReloadAllCommandsAsync();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user