Compare commits

..

1 Commits

Author SHA1 Message Date
vanzue
4e4d0a610f wire things up for cmdpal & kbm 2026-03-12 18:11:41 +08:00
123 changed files with 3702 additions and 3929 deletions

View File

@@ -1,7 +1,6 @@
# COLORS
argb
Bgr
bgra
BLACKONWHITE
BLUEGRAY
@@ -29,7 +28,6 @@ RUS
AYUV
bak
HDP
Bcl
bgcode
Deflatealgorithm
@@ -48,7 +46,6 @@ nupkg
petabyte
resw
resx
runtimeconfig
srt
Stereolithography
terabyte
@@ -300,8 +297,6 @@ pwa
AOT
Aot
ify
TFM
# YML
onefuzz

View File

@@ -16,7 +16,6 @@ adaptivecards
ADDSTRING
ADDUNDORECORD
ADifferent
ADMINS
adml
admx
advfirewall
@@ -170,11 +169,7 @@ cim
CImage
cla
CLASSDC
classguid
classmethod
CLASSNOTAVAILABLE
claude
CLEARTYPE
clickable
clickonce
clientside
@@ -206,7 +201,6 @@ colorformat
colorhistory
colorhistorylimit
COLORKEY
colorref
comctl
comdlg
comexp
@@ -224,7 +218,6 @@ CONTEXTHELP
CONTEXTMENUHANDLER
contractversion
CONTROLPARENT
Convs
cooldown
copiedcolorrepresentation
COPYPEN
@@ -236,8 +229,6 @@ cpcontrols
cph
cplusplus
CPower
cpptools
cppvsdbg
cppwinrt
createdump
CREATEPROCESS
@@ -260,8 +251,6 @@ CTLCOLORSTATIC
CURRENTDIR
CURSORINFO
cursorpos
CURSORSHOWING
cursorwrap
customaction
CUSTOMACTIONTEST
CVal
@@ -278,14 +267,12 @@ dacl
datareader
datatracker
Dayof
dbcc
DBID
DBLCLKS
DBLEPSILON
DBPROP
DBPROPIDSET
DBPROPSET
DBT
DCBA
DCOM
DComposition
@@ -301,8 +288,6 @@ DEFAULTFLAGS
DEFAULTICON
defaultlib
DEFAULTONLY
DEFAULTSIZE
defaulttonearest
DEFAULTTONULL
DEFAULTTOPRIMARY
DEFERERASE
@@ -322,21 +307,11 @@ DESKTOPABSOLUTEPARSING
desktopshorcutinstalled
devblogs
devdocs
devenv
DEVICEINTERFACE
devicetype
DEVINTERFACE
devmgmt
DEVMODE
DEVMODEW
DEVNODES
devpal
DEVTYP
dfx
DIALOGEX
diffs
digicert
DINORMAL
DISABLEASACTIONKEY
DISABLENOSCROLL
diskmgmt
@@ -450,12 +425,6 @@ eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FARPROC
fdw
fdx
FErase
fesf
FFFF
Figma
FILEEXPLORER
FILEFLAGS
FILEFLAGSMASK
@@ -472,7 +441,6 @@ FILESYSPATH
Filetime
FILEVERSION
FILTERMODE
FInc
findfast
FIXEDFILEINFO
FIXEDSYS
@@ -528,7 +496,6 @@ GPOCA
gpp
gpu
gradians
GRGX
GSM
gtm
guiddata
@@ -559,13 +526,11 @@ HCRYPTPROV
hcursor
hcwhite
hdc
HDEVNOTIFY
hdr
hdrop
hdwwiz
Helpline
helptext
hgdiobj
HGFE
hglobal
hhk
@@ -710,12 +675,12 @@ jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
jjw
jobject
JOBOBJECT
jpe
jpnime
Jsons
jsonval
jxr
kbmcontrols
keybd
KEYBDDATA
KEYBDINPUT
@@ -744,7 +709,6 @@ Ldone
Ldr
LEFTSCROLLBAR
LEFTTEXT
leftclick
LError
LEVELID
LExit
@@ -776,8 +740,6 @@ lowlevel
LOWORD
lparam
LPBITMAPINFOHEADER
LPCFHOOKPROC
lpch
LPCITEMIDLIST
LPCLSID
lpcmi
@@ -795,7 +757,6 @@ LPMONITORINFO
LPOSVERSIONINFOEXW
LPQUERY
lprc
LPrivate
LPSAFEARRAY
lpstr
lpsz
@@ -842,8 +803,6 @@ MAXSHORTCUTSIZE
maxversiontested
MBM
MBR
Mbuttondown
mcp
MDICHILD
MDL
mdtext
@@ -855,13 +814,11 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metacharacter
metadatamatters
Metadatas
Metacharacter
metafile
metapackage
mfc
mfalse
Mgmt
Microwaved
midl
@@ -884,7 +841,6 @@ mmsys
mobileredirect
mockapi
MODALFRAME
modelcontextprotocol
MODESPRUNED
MONITORENUMPROC
MONITORINFO
@@ -919,10 +875,9 @@ MSLLHOOKSTRUCT
Mso
msrc
msstore
mstsc
msvcp
MT
MTND
mtrue
MULTIPLEUSE
multizone
muxc
@@ -930,8 +885,6 @@ mvvm
MVVMTK
MWBEx
MYICON
myorg
myrepo
NAMECHANGE
namespaceanddescendants
nao
@@ -1046,8 +999,6 @@ OEMCONVERT
officehubintl
OFN
ofs
OICI
OICIIO
oldcolor
olditem
oldpath
@@ -1058,7 +1009,6 @@ openas
opencode
OPENFILENAME
opensource
openurl
openxmlformats
OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE
@@ -1081,7 +1031,6 @@ Packagemanager
PACL
padx
pady
PAI
PAINTSTRUCT
PALETTEWINDOW
PARENTNOTIFY
@@ -1254,7 +1203,6 @@ RAWPATH
rbhid
rclsid
RCZOOMIT
rdp
RDW
READMODE
READOBJECTS
@@ -1282,7 +1230,6 @@ remappings
REMAPSUCCESSFUL
REMAPUNSUCCESSFUL
Remotable
remotedesktop
remoteip
Removelnk
renamable
@@ -1313,7 +1260,6 @@ RIGHTSCROLLBAR
riid
RKey
RNumber
rollups
rop
ROUNDSMALL
rpcrt
@@ -1346,7 +1292,7 @@ SCREENFONTS
screensaver
screenshots
scrollviewer
sddl
SDDL
SDKDDK
sdns
searchterm
@@ -1525,9 +1471,6 @@ SVGIO
svgz
SVSI
SWFO
swp
SWPNOSIZE
SWPNOZORDER
SWRESTORE
symbolrequestprod
SYMCACHE
@@ -1544,8 +1487,6 @@ SYSKEY
syskeydown
SYSKEYUP
SYSLIB
sysmenu
systemai
SYSTEMAPPS
SYSTEMMODAL
SYSTEMTIME
@@ -1632,9 +1573,6 @@ UHash
UIA
UIEx
ULONGLONG
Ultrawide
UMax
UMin
ums
uncompilable
UNCPRIORITY

View File

@@ -1,165 +0,0 @@
---
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 |

View File

@@ -1,287 +0,0 @@
# 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 |

View File

@@ -1,226 +0,0 @@
# 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 -->
```

View File

@@ -1,516 +0,0 @@
# 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)

View File

@@ -1,314 +0,0 @@
# 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)
```

View File

@@ -1,365 +0,0 @@
# 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
View File

@@ -360,8 +360,3 @@ 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/

View File

@@ -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,6 +40,7 @@
<!-- 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" />

256
README.md
View File

@@ -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.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
[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
| Description | Filename |
|----------------|----------|
| 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] |
| 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] |
</details>
@@ -102,14 +102,242 @@ 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
[![What's new image](doc/images/readme/Release-Banner.png)](https://github.com/microsoft/PowerToys/releases)
**Version 0.97.2 (Feb 2026)**
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.0).
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 doesnt duplicate entries.
- Prevented placeholder endpoints from being saved for providers that dont 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 nonbreaking 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 thirdparty list. Thanks [@artickc](https://github.com/artickc)!
- Added the Open With Antigravity plugin to the thirdparty 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.
## 🛣️ 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.99][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.98][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!

View File

@@ -0,0 +1,308 @@
# Keyboard Manager CmdPal Integration
## Goal
Expose Keyboard Manager mappings in Command Palette (`cmdpal`) through `ext.powertoys` with two separate user experiences:
- quick actions for executable mappings
- an inspection list for all current mappings
This should be done without introducing a new settings schema or a CmdPal-specific Keyboard Manager settings file.
The first scope should cover:
- Expose `Run Program` remaps as invokable CmdPal actions
- Expose `Open URI` remaps as invokable CmdPal actions
- Add one Keyboard Manager `List all mappings` command item
- List all current mappings on a dedicated Keyboard Manager page
- Make the primary interaction for a mapping item be inspection of what that mapping does
## Current State
The repository already contains most of the plumbing needed for this integration:
1. Keyboard Manager publishes actions through the module action surface in `src/modules/keyboardmanager/dll/dllmain.cpp`.
2. Runner aggregates module actions through `src/runner/action_registry.cpp` and exposes them over the existing named pipe consumed by `RunnerActionClient`.
3. The PowerToys CmdPal extension enumerates module commands through `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs`.
4. Keyboard Manager already has a provider in `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs` that:
- shows the active-state toggle
- opens the new editor
- enumerates Runner actions with the `powertoys.keyboardManager.mapping.` prefix
- invokes them through `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/KeyboardManager/InvokeKeyboardManagerCustomActionCommand.cs`
At the Keyboard Manager layer, only remappings backed by executable actions are currently turned into invokable actions:
- `Shortcut::IsRunProgram()`
- `Shortcut::IsOpenURI()`
This is implemented in `is_keyboard_manager_custom_action`, `append_mapping_actions`, and `invoke_keyboard_manager_custom_action` in `src/modules/keyboardmanager/dll/dllmain.cpp`.
The repository also already has read-side Keyboard Manager mapping logic in `src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs`, but that code lives inside the editor UI project and is not an appropriate dependency for `ext.powertoys`.
## Lightweight Design
### Design Decision
Split the design into two surfaces:
1. Executable mapping actions
2. Mapping inspection
Executable mapping actions stay centered on the existing Runner action registry.
Mapping inspection should use a dedicated read-only Keyboard Manager query service shared with CmdPal, following the existing `*.ModuleServices` pattern used by other modules.
Do not add:
- a new CmdPal-only data file
- a direct JSON parse of Keyboard Manager settings in `ext.powertoys`
- a UI-project dependency from `ext.powertoys` to `KeyboardManagerEditorUI`
- CmdPal-specific logic in the Keyboard Manager editor
### Why This Is The Right Shape
This matches the way other PowerToys modules integrate with CmdPal:
- module owns its state and execution semantics
- executable actions are published through a small action surface
- richer read-only data can be exposed through a shared service layer when the module needs inspection or navigation
This keeps the startup path lean and avoids duplicating Keyboard Manager parsing logic in `ext.powertoys`.
## Proposed Functional Model
### Surface A: Executable Actions
Keyboard Manager remains the source of truth for which mappings are eligible for direct invocation in CmdPal.
When `get_actions()` is called:
1. Load the current `MappingConfiguration`
2. Enumerate OS-level shortcut remaps
3. Enumerate app-specific shortcut remaps
4. Keep only remaps whose target operation is:
- `Run Program`
- `Open URI`
5. Emit one Runner action descriptor per eligible remap
### Identity For Executable Actions
Each action id remains derived from the remap identity:
- source shortcut
- exact-match flag
- app scope
- target operation type
- target payload fields
The current implementation uses a hashed identity under the prefix `powertoys.keyboardManager.mapping.`. That is acceptable for a lightweight design because:
- CmdPal does not need stable ids across edits beyond the current session
- the action id is regenerated from source-of-truth settings
- action invocation already re-resolves the action against current config and fails safely if the mapping no longer exists
### Invocation Of Executable Actions
CmdPal invokes the selected item through `RunnerActionClient.InvokeAction(actionId)`.
Keyboard Manager stays responsible for:
- launching programs
- open-existing-instance behavior
- elevation mode
- start-in directory
- window visibility
- URI/path normalization and shell execution
This is important because CmdPal should not duplicate Keyboard Manager's execution semantics.
### Surface B: All Mappings Inspection
CmdPal also needs one dedicated Keyboard Manager entry for inspecting every current mapping, not just executable ones.
That entry should be a top-level module item such as:
- `List Keyboard Manager mappings`
Its command should open a dedicated `KeyboardManagerMappingsPage`.
### Data Source For All Mappings
The `KeyboardManagerMappingsPage` should not be backed by Runner actions because Runner actions currently model invokable operations only.
Instead, add a small shared Keyboard Manager query layer, ideally as a module service project, for example:
- `src/modules/keyboardmanager/KeyboardManager.ModuleServices`
That shared service should reuse the existing native mapping query path already used by the editor and expose normalized read-only DTOs for CmdPal consumption.
The service should cover all current mapping categories:
- single key to key
- single key to shortcut
- single key to text
- shortcut to shortcut
- shortcut to program
- shortcut to URI
- app-specific shortcut mappings
### Interaction Model For The Mappings Page
The mappings page should behave like an inspection page first, not an execution page first.
Recommended interaction:
1. `KeyboardManagerMappingsPage` is a `DynamicListPage` or `ListPage` with `ShowDetails = true`
2. Each mapping is rendered as a `ListItem` with rich `Details`
3. Selecting a mapping shows what it maps to
4. Invoking the item opens a small `KeyboardManagerMappingDetailsPage` or equivalent detail-focused page
5. Executable mappings may expose an extra command such as `Run now` or `Open now`, but that should not be the primary action on the inspection page
This satisfies the requirement that the primary action for a mapping entry is to show what the mapping is, while still leaving room for execution when the mapping type supports it.
### Presentation In CmdPal
Within `ext.powertoys`, the Keyboard Manager provider should emit these command groups:
1. Keyboard Manager state commands
- toggle active state
- open editor
2. Keyboard Manager inspection commands
- `List Keyboard Manager mappings`
3. Keyboard Manager quick actions
- `Run Program` entries
- `Open URI` entries
4. Keyboard Manager settings
- open settings
## UX Guidance
The minimum viable experience is:
- searchable by trigger or target
- clearly labeled as Keyboard Manager actions or mappings
- capable of both inspection and direct execution for supported mapping types
Recommended presentation rules:
1. The `List Keyboard Manager mappings` item should use the Keyboard Manager icon and clearly signal it opens a list, not an action.
2. Mapping list titles should prioritize the trigger:
- `Ctrl+Alt+N`
- `Caps Lock`
3. Mapping list subtitles should say what the trigger maps to:
- `Opens notepad.exe`
- `Maps to Ctrl+C`
- `Types Hello world`
4. Mapping details should carry the rest of the context:
- global vs app-specific
- mapping kind
- target payload
- execution-specific options when relevant
5. Quick-action titles can continue to prioritize the executable action:
- `Run notepad.exe`
- `Open https://contoso.com`
6. Keyboard Manager module icon is sufficient for the first version
The current implementation already covers the quick-action portion. The new work is primarily the all-mappings inspection surface.
## Non-Goals For The First Version
Do not add these in the initial pass:
- editing Keyboard Manager mappings from CmdPal
- enabling or disabling individual mappings from CmdPal
- live push notifications when mappings change
- custom icons per program or URI
- a new `kbm:` command syntax or dedicated parser
These all increase complexity without being necessary to validate the feature.
## Integration Pattern Compared To Other Modules
This feature now combines two existing CmdPal integration styles.
- `Workspaces` loads module-owned data and emits one command per data item
- `FancyZones` uses dedicated pages and details for richer inspection
Keyboard Manager quick actions should follow the lighter `Workspaces` pattern:
- one provider
- one flat list of dynamic items
- generic command invocation
Keyboard Manager all-mappings inspection should follow the `list page with details` pattern already supported by CmdPal:
- one top-level entry that opens a page
- one list item per mapping
- rich `Details` on every row
- optional secondary commands for invokable mappings
The main constraint is the same in both paths: `ext.powertoys` should not duplicate Keyboard Manager's mapping schema by parsing the settings file directly.
## Error Handling
The existing executable-action behavior is the correct baseline:
- hidden or deleted mappings simply disappear from `list_actions`
- stale CmdPal entries fail through `action_not_found`
- disabled Keyboard Manager returns `module_unavailable`
- launch failures return module-defined error messages
CmdPal only needs to surface the returned message as toast text for quick actions.
For the all-mappings inspection page:
- malformed or unreadable mapping snapshots should yield an empty page or an inline error item
- missing targets should still render as mappings, but be tagged as invalid or unavailable
- details rendering should degrade gracefully when optional fields are absent
## Risks
### Two Data Paths
This design intentionally uses two integration paths:
- Runner actions for invokable mappings
- a shared read-only query service for all mappings
That is acceptable because the two paths serve different UX needs. The risk is manageable as long as both paths derive from the same Keyboard Manager mapping model rather than separate ad hoc parsers.
### Duplicate Or Ambiguous Entries
Different mappings may produce similar titles, especially on the quick-action side. This is acceptable in the first iteration because the subtitle already carries scope and trigger details.
### Action Id Churn After Edits
Editing a mapping changes the derived id. This is acceptable because action ids are not a persisted public contract.
### Large Mapping Sets
Very large mapping sets could make the inspection page noisy. This is manageable for the first version if the page supports search and details, but sectioning or filters may be needed later.
## Minimal Implementation Plan
1. Keep Keyboard Manager as the producer of invokable mapping actions.
2. Keep Runner action registry as the discovery path for executable quick actions.
3. Add a small shared read-only Keyboard Manager module service for enumerating all mappings.
4. Add `List Keyboard Manager mappings` to `KeyboardManagerModuleCommandProvider`.
5. Add a `KeyboardManagerMappingsPage` with `ShowDetails = true`.
6. Represent each mapping as an inspection-first `ListItem` with rich `Details`.
7. Add optional secondary execution commands only for mappings that are invokable.
8. Add focused tests around:
- Keyboard Manager action enumeration for `Run Program` and `Open URI`
- mapping snapshot enumeration across all mapping kinds
- CmdPal rendering of the mappings page
- graceful handling of stale, invalid, or missing mappings
## Future Extensions
If the first version lands well, the next step should still preserve the same split architecture:
- enrich action descriptors with more metadata if Runner actions grow argument or icon support
- add sections or filters to the mappings page when the list becomes large
- optionally expose app-specific filtering in CmdPal UI
The extension points should remain:
- Runner actions for execution
- a shared Keyboard Manager query service for inspection

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

View File

@@ -22,16 +22,6 @@
<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" />

View File

@@ -3,28 +3,18 @@
<?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" />
@@ -67,7 +57,6 @@
<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>

View File

@@ -174,9 +174,7 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
#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"

View File

@@ -0,0 +1,193 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using PowerToys.Interop;
namespace ManagedCommon
{
public sealed class RunnerActionClient
{
public IReadOnlyList<RunnerActionDescriptor> ListActions()
{
var response = SendRequest("list_actions", string.Empty, "{}");
return response.Success && response.Actions.Count > 0 ? response.Actions : Array.Empty<RunnerActionDescriptor>();
}
public RunnerActionInvokeResult InvokeAction(string actionId, string serializedArguments = "{}")
{
if (string.IsNullOrWhiteSpace(actionId))
{
return new RunnerActionInvokeResult
{
Success = false,
ErrorCode = "invalid_action_id",
Message = "Action id is required.",
};
}
var response = SendRequest("invoke_action", actionId, string.IsNullOrWhiteSpace(serializedArguments) ? "{}" : serializedArguments);
return new RunnerActionInvokeResult
{
Success = response.Success,
ErrorCode = response.ErrorCode,
Message = response.Message,
};
}
private static RunnerActionResponse SendRequest(string requestType, string actionId, string arguments)
{
var pipeName = Path.GetFileName(Constants.PowerToysActionsPipe());
using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None);
pipe.Connect(2000);
var payload = BuildRequestPayload(requestType, actionId, arguments);
var lengthBuffer = BitConverter.GetBytes(payload.Length);
pipe.Write(lengthBuffer, 0, lengthBuffer.Length);
pipe.Write(payload, 0, payload.Length);
pipe.Flush();
var responseLengthBuffer = ReadExact(pipe, sizeof(int));
var responseLength = BitConverter.ToInt32(responseLengthBuffer, 0);
var responsePayload = responseLength == 0 ? Array.Empty<byte>() : ReadExact(pipe, responseLength);
return ParseResponse(responsePayload);
}
private static byte[] BuildRequestPayload(string requestType, string actionId, string arguments)
{
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream))
{
writer.WriteStartObject();
writer.WriteString("type", requestType);
if (!string.IsNullOrWhiteSpace(actionId))
{
writer.WriteString("action_id", actionId);
}
writer.WriteString("arguments", arguments);
writer.WriteEndObject();
}
return stream.ToArray();
}
private static RunnerActionResponse ParseResponse(byte[] payload)
{
if (payload.Length == 0)
{
return RunnerActionResponse.CreateError("empty_response", "Runner returned an empty response.");
}
using var document = JsonDocument.Parse(payload);
var root = document.RootElement;
var response = new RunnerActionResponse();
if (root.TryGetProperty("success", out var successElement) &&
(successElement.ValueKind == JsonValueKind.True || successElement.ValueKind == JsonValueKind.False))
{
response.Success = successElement.GetBoolean();
}
if (root.TryGetProperty("error_code", out var errorCodeElement) && errorCodeElement.ValueKind == JsonValueKind.String)
{
response.ErrorCode = errorCodeElement.GetString() ?? string.Empty;
}
if (root.TryGetProperty("message", out var messageElement) && messageElement.ValueKind == JsonValueKind.String)
{
response.Message = messageElement.GetString() ?? string.Empty;
}
if (root.TryGetProperty("actions", out var actionsElement) && actionsElement.ValueKind == JsonValueKind.Array)
{
response.Actions = ParseActions(actionsElement);
}
return response;
}
private static List<RunnerActionDescriptor> ParseActions(JsonElement actionsElement)
{
var actions = new List<RunnerActionDescriptor>();
foreach (var element in actionsElement.EnumerateArray())
{
if (element.ValueKind != JsonValueKind.Object)
{
continue;
}
actions.Add(new RunnerActionDescriptor
{
ActionId = GetStringProperty(element, "action_id"),
ModuleKey = GetStringProperty(element, "module_key"),
DisplayName = GetStringProperty(element, "display_name"),
Description = GetStringProperty(element, "description"),
Category = GetStringProperty(element, "category"),
Available = GetBoolProperty(element, "available"),
});
}
return actions;
}
private static string GetStringProperty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.String
? property.GetString() ?? string.Empty
: string.Empty;
}
private static bool GetBoolProperty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var property) &&
(property.ValueKind == JsonValueKind.True || property.ValueKind == JsonValueKind.False) &&
property.GetBoolean();
}
private static byte[] ReadExact(Stream stream, int length)
{
var buffer = new byte[length];
var offset = 0;
while (offset < length)
{
var bytesRead = stream.Read(buffer, offset, length - offset);
if (bytesRead == 0)
{
throw new EndOfStreamException("Unexpected end of stream while reading runner action response.");
}
offset += bytesRead;
}
return buffer;
}
private sealed class RunnerActionResponse
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public List<RunnerActionDescriptor> Actions { get; set; } = new();
public static RunnerActionResponse CreateError(string errorCode, string message)
{
return new RunnerActionResponse
{
Success = false,
ErrorCode = errorCode,
Message = message,
};
}
}
}
}

View File

@@ -0,0 +1,21 @@
// 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 ManagedCommon
{
public sealed class RunnerActionDescriptor
{
public string ActionId { get; set; } = string.Empty;
public string ModuleKey { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public bool Available { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ManagedCommon
{
public static class RunnerActionIds
{
public const string KeyboardManagerToggleActive = "powertoys.keyboardManager.toggleActive";
public const string KeyboardManagerOpenEditor = "powertoys.keyboardManager.openEditor";
}
}

View File

@@ -0,0 +1,15 @@
// 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 ManagedCommon
{
public sealed class RunnerActionInvokeResult
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
}

View File

@@ -308,5 +308,9 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX;
}
hstring Constants::PowerToysActionsPipe()
{
return CommonSharedConstants::POWERTOYS_ACTIONS_PIPE;
}
}

View File

@@ -80,6 +80,7 @@ namespace winrt::PowerToys::Interop::implementation
static hstring OpenNewKeyboardManagerEvent();
static hstring ToggleKeyboardManagerActiveEvent();
static hstring KeyboardManagerEngineInstanceMutex();
static hstring PowerToysActionsPipe();
};
}

View File

@@ -77,6 +77,7 @@ namespace PowerToys
static String OpenNewKeyboardManagerEvent();
static String ToggleKeyboardManagerActiveEvent();
static String KeyboardManagerEngineInstanceMutex();
static String PowerToysActionsPipe();
}
}
}

View File

@@ -174,6 +174,7 @@ namespace CommonSharedConstants
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";
const wchar_t POWERTOYS_ACTIONS_PIPE[] = L"\\\\.\\pipe\\PowerToysActionsPipe-e98c2e3d-52ab-4f9b-a65b-5b9bb6f0e312";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";

View File

@@ -16,6 +16,7 @@ using System.Threading.Tasks;
using HostsUILib.Exceptions;
using HostsUILib.Models;
using HostsUILib.Settings;
using Microsoft.Win32;
namespace HostsUILib.Helpers
{
@@ -222,17 +223,64 @@ namespace HostsUILib.Helpers
public void OpenHostsFile()
{
var notepadFallback = false;
try
{
var notepadPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Windows),
"System32",
"notepad.exe");
Process.Start(new ProcessStartInfo(notepadPath, HostsFilePath));
// 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;
}
}
}
catch (Exception ex)
{
LoggerInstance.Logger.LogError("Failed to open notepad", 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);
}
}
}

View File

@@ -14,6 +14,7 @@ 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();
@@ -691,6 +692,7 @@ int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativ
return static_cast<int>(result);
}
std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const
{
std::vector<GapInfo> gaps;

View File

@@ -5,23 +5,20 @@
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, ISettingsService settingsService)
public AliasManager(TopLevelCommandManager tlcManager, SettingsModel settings)
{
_topLevelCommandManager = tlcManager;
_settingsService = settingsService;
_aliases = _settingsService.Settings.Aliases;
_aliases = settings.Aliases;
if (_aliases.Count == 0)
{

View File

@@ -1,14 +1,26 @@
// 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)!
@@ -19,4 +31,141 @@ 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");
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -87,8 +87,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
Color.FromArgb(255, 126, 115, 95), // #7e735f
];
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settings;
private readonly UISettings _uiSettings;
private readonly IThemeService _themeService;
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
@@ -101,18 +100,18 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int ThemeIndex
{
get => (int)_settingsService.Settings.Theme;
get => (int)_settings.Theme;
set => Theme = (UserTheme)value;
}
public UserTheme Theme
{
get => _settingsService.Settings.Theme;
get => _settings.Theme;
set
{
if (_settingsService.Settings.Theme != value)
if (_settings.Theme != value)
{
_settingsService.Settings.Theme = value;
_settings.Theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ThemeIndex));
Save();
@@ -122,12 +121,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public ColorizationMode ColorizationMode
{
get => _settingsService.Settings.ColorizationMode;
get => _settings.ColorizationMode;
set
{
if (_settingsService.Settings.ColorizationMode != value)
if (_settings.ColorizationMode != value)
{
_settingsService.Settings.ColorizationMode = value;
_settings.ColorizationMode = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorizationModeIndex));
OnPropertyChanged(nameof(IsCustomTintVisible));
@@ -153,18 +152,18 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int ColorizationModeIndex
{
get => (int)_settingsService.Settings.ColorizationMode;
get => (int)_settings.ColorizationMode;
set => ColorizationMode = (ColorizationMode)value;
}
public Color ThemeColor
{
get => _settingsService.Settings.CustomThemeColor;
get => _settings.CustomThemeColor;
set
{
if (_settingsService.Settings.CustomThemeColor != value)
if (_settings.CustomThemeColor != value)
{
_settingsService.Settings.CustomThemeColor = value;
_settings.CustomThemeColor = value;
OnPropertyChanged();
@@ -180,10 +179,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int ColorIntensity
{
get => _settingsService.Settings.CustomThemeColorIntensity;
get => _settings.CustomThemeColorIntensity;
set
{
_settingsService.Settings.CustomThemeColorIntensity = value;
_settings.CustomThemeColorIntensity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveTintIntensity));
Save();
@@ -192,10 +191,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageTintIntensity
{
get => _settingsService.Settings.BackgroundImageTintIntensity;
get => _settings.BackgroundImageTintIntensity;
set
{
_settingsService.Settings.BackgroundImageTintIntensity = value;
_settings.BackgroundImageTintIntensity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveTintIntensity));
Save();
@@ -204,12 +203,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public string BackgroundImagePath
{
get => _settingsService.Settings.BackgroundImagePath ?? string.Empty;
get => _settings.BackgroundImagePath ?? string.Empty;
set
{
if (_settingsService.Settings.BackgroundImagePath != value)
if (_settings.BackgroundImagePath != value)
{
_settingsService.Settings.BackgroundImagePath = value;
_settings.BackgroundImagePath = value;
OnPropertyChanged();
if (BackgroundImageOpacity == 0)
@@ -224,12 +223,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageOpacity
{
get => _settingsService.Settings.BackgroundImageOpacity;
get => _settings.BackgroundImageOpacity;
set
{
if (_settingsService.Settings.BackgroundImageOpacity != value)
if (_settings.BackgroundImageOpacity != value)
{
_settingsService.Settings.BackgroundImageOpacity = value;
_settings.BackgroundImageOpacity = value;
OnPropertyChanged();
Save();
}
@@ -238,12 +237,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageBrightness
{
get => _settingsService.Settings.BackgroundImageBrightness;
get => _settings.BackgroundImageBrightness;
set
{
if (_settingsService.Settings.BackgroundImageBrightness != value)
if (_settings.BackgroundImageBrightness != value)
{
_settingsService.Settings.BackgroundImageBrightness = value;
_settings.BackgroundImageBrightness = value;
OnPropertyChanged();
Save();
}
@@ -252,12 +251,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageBlurAmount
{
get => _settingsService.Settings.BackgroundImageBlurAmount;
get => _settings.BackgroundImageBlurAmount;
set
{
if (_settingsService.Settings.BackgroundImageBlurAmount != value)
if (_settings.BackgroundImageBlurAmount != value)
{
_settingsService.Settings.BackgroundImageBlurAmount = value;
_settings.BackgroundImageBlurAmount = value;
OnPropertyChanged();
Save();
}
@@ -266,12 +265,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public BackgroundImageFit BackgroundImageFit
{
get => _settingsService.Settings.BackgroundImageFit;
get => _settings.BackgroundImageFit;
set
{
if (_settingsService.Settings.BackgroundImageFit != value)
if (_settings.BackgroundImageFit != value)
{
_settingsService.Settings.BackgroundImageFit = value;
_settings.BackgroundImageFit = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BackgroundImageFitIndex));
Save();
@@ -300,12 +299,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackdropOpacity
{
get => _settingsService.Settings.BackdropOpacity;
get => _settings.BackdropOpacity;
set
{
if (_settingsService.Settings.BackdropOpacity != value)
if (_settings.BackdropOpacity != value)
{
_settingsService.Settings.BackdropOpacity = value;
_settings.BackdropOpacity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveBackdropStyle));
OnPropertyChanged(nameof(EffectiveImageOpacity));
@@ -316,13 +315,13 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackdropStyleIndex
{
get => (int)_settingsService.Settings.BackdropStyle;
get => (int)_settings.BackdropStyle;
set
{
var newStyle = (BackdropStyle)value;
if (_settingsService.Settings.BackdropStyle != newStyle)
if (_settings.BackdropStyle != newStyle)
{
_settingsService.Settings.BackdropStyle = newStyle;
_settings.BackdropStyle = newStyle;
OnPropertyChanged();
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
@@ -344,25 +343,25 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
/// Gets whether the backdrop opacity slider should be visible.
/// </summary>
public bool IsBackdropOpacityVisible =>
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether the backdrop description (for styles without options) should be visible.
/// </summary>
public bool IsMicaBackdropDescriptionVisible =>
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
!BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether background/colorization settings are available.
/// </summary>
public bool IsBackgroundSettingsEnabled =>
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
/// <summary>
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
/// </summary>
public bool IsBackgroundNotAvailableVisible =>
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
public BackdropStyle? EffectiveBackdropStyle
{
@@ -371,9 +370,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 (_settingsService.Settings.BackdropStyle != BackdropStyle.Acrylic || _settingsService.Settings.BackdropOpacity < 100)
if (_settings.BackdropStyle != BackdropStyle.Acrylic || _settings.BackdropOpacity < 100)
{
return _settingsService.Settings.BackdropStyle;
return _settings.BackdropStyle;
}
return null;
@@ -382,39 +381,39 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public double EffectiveImageOpacity =>
EffectiveBackdropStyle is not null
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settingsService.Settings.BackdropOpacity / 100.0)
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settings.BackdropOpacity / 100.0)
: (BackgroundImageOpacity / 100f);
[ObservableProperty]
public partial bool IsColorizationDetailsExpanded { get; set; }
public bool IsCustomTintVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsColorIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
public bool IsImageTintIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
public bool IsImageTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.Image;
/// <summary>
/// Gets the effective tint intensity for the preview, based on the current colorization mode.
/// </summary>
public int EffectiveTintIntensity => _settingsService.Settings.ColorizationMode is ColorizationMode.Image
? _settingsService.Settings.BackgroundImageTintIntensity
: _settingsService.Settings.CustomThemeColorIntensity;
public int EffectiveTintIntensity => _settings.ColorizationMode is ColorizationMode.Image
? _settings.BackgroundImageTintIntensity
: _settings.CustomThemeColorIntensity;
public bool IsBackgroundControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
public bool IsNoBackgroundVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.None;
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
public bool IsAccentColorControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public bool IsResetButtonVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
public bool IsResetButtonVisible => _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(_settingsService.Settings.BackdropStyle).SupportsColorization
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
? Colors.Transparent
: ColorizationMode switch
{
@@ -429,7 +428,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
public ImageSource? EffectiveBackgroundImageSource =>
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsBackgroundImage
!BackdropStyles.Get(_settings.BackdropStyle).SupportsBackgroundImage
? null
: ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
@@ -437,11 +436,11 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
public AppearanceSettingsViewModel(IThemeService themeService, ISettingsService settingsService)
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
{
_themeService = themeService;
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
_settingsService = settingsService;
_settings = settings;
_uiSettings = new UISettings();
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
@@ -449,7 +448,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
Reapply();
IsColorizationDetailsExpanded = _settingsService.Settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
}
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
@@ -470,7 +469,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
private void Save()
{
_settingsService.Save();
SettingsModel.SaveSettings(_settings);
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}

View File

@@ -139,8 +139,7 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
return;
}
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
IsActive = providerSettings.IsEnabled;
@@ -250,15 +249,16 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
IServiceProvider serviceProvider,
ICommandProvider4? four)
{
var settings = serviceProvider.GetRequiredService<ISettingsService>().Settings;
var settings = serviceProvider.GetService<SettingsModel>()!;
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, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
topLevelViewModel.InitializeProperties();
return topLevelViewModel;
@@ -407,8 +407,7 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
public void PinCommand(string commandId, IServiceProvider serviceProvider)
{
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
if (!providerSettings.PinnedCommandIds.Contains(commandId))
@@ -417,14 +416,13 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
settingsService.Save(hotReload: false);
SettingsModel.SaveSettings(settings, false);
}
}
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
{
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var settings = serviceProvider.GetService<SettingsModel>()!;
var providerSettings = GetProviderSettings(settings);
if (providerSettings.PinnedCommandIds.Remove(commandId))
@@ -432,14 +430,13 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
settingsService.Save(hotReload: false);
SettingsModel.SaveSettings(settings, false);
}
}
public void PinDockBand(string commandId, IServiceProvider serviceProvider)
{
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var settings = serviceProvider.GetService<SettingsModel>()!;
var bandSettings = new DockBandSettings
{
CommandId = commandId,
@@ -450,20 +447,19 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
settingsService.Save(hotReload: false);
SettingsModel.SaveSettings(settings, false);
}
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider)
{
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var settings = serviceProvider.GetService<SettingsModel>()!;
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));
settingsService.Save(hotReload: false);
SettingsModel.SaveSettings(settings, false);
}
public ICommandProviderContext GetProviderContext() => this;

View File

@@ -18,7 +18,6 @@ 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;
@@ -43,8 +42,8 @@ public sealed partial class MainListPage : DynamicListPage,
private readonly ThrottledDebouncedAction _refreshThrottledDebouncedAction;
private readonly TopLevelCommandManager _tlcManager;
private readonly AliasManager _aliasManager;
private readonly ISettingsService _settingsService;
private readonly IAppStateService _appStateService;
private readonly SettingsModel _settings;
private readonly AppStateModel _appStateModel;
private readonly ScoringFunction<IListItem> _scoringFunction;
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
@@ -80,23 +79,23 @@ public sealed partial class MainListPage : DynamicListPage,
public MainListPage(
TopLevelCommandManager topLevelCommandManager,
SettingsModel settings,
AliasManager aliasManager,
IFuzzyMatcherProvider fuzzyMatcherProvider,
ISettingsService settingsService,
IAppStateService appStateService)
AppStateModel appStateModel,
IFuzzyMatcherProvider fuzzyMatcherProvider)
{
Id = "com.microsoft.cmdpal.home";
Title = Resources.builtin_home_name;
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
_settingsService = settingsService;
_settings = settings;
_aliasManager = aliasManager;
_appStateService = appStateService;
_appStateModel = appStateModel;
_tlcManager = topLevelCommandManager;
_fuzzyMatcherProvider = fuzzyMatcherProvider;
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateService.State.RecentCommands, _fuzzyMatcherProvider.Current);
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settingsService.Settings.FallbackRanks);
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateModel.RecentCommands, _fuzzyMatcherProvider.Current);
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settings.FallbackRanks);
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
@@ -151,8 +150,8 @@ public sealed partial class MainListPage : DynamicListPage,
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
_settingsService.SettingsChanged += SettingsChangedHandler;
HotReloadSettings(_settingsService.Settings);
settings.SettingsChanged += SettingsChangedHandler;
HotReloadSettings(settings);
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
IsLoading = true;
@@ -365,7 +364,7 @@ public sealed partial class MainListPage : DynamicListPage,
}
// prefilter fallbacks
var globalFallbacks = _settingsService.Settings.GetGlobalFallbacks();
var globalFallbacks = _settings.GetGlobalFallbacks();
var specialFallbacks = new List<TopLevelViewModel>(globalFallbacks.Length);
var commonFallbacks = new List<TopLevelViewModel>(commands.Count - globalFallbacks.Length);
@@ -480,7 +479,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.
_settingsService.Settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
_settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
var pinnedCommandIds = providerSettings?.PinnedCommandIds;
if (pinnedCommandIds is not null && pinnedCommandIds.Count > 0)
@@ -679,9 +678,9 @@ public sealed partial class MainListPage : DynamicListPage,
public void UpdateHistory(IListItem topLevelOrAppItem)
{
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
var history = _appStateService.State.RecentCommands;
var history = _appStateModel.RecentCommands;
history.AddHistoryItem(id);
_appStateService.Save();
AppStateModel.SaveState(_appStateModel);
}
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
@@ -704,7 +703,7 @@ public sealed partial class MainListPage : DynamicListPage,
RequestRefresh(fullRefresh: false);
}
private void SettingsChangedHandler(ISettingsService sender, SettingsModel args) => HotReloadSettings(args);
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender);
private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails;
@@ -717,9 +716,9 @@ public sealed partial class MainListPage : DynamicListPage,
_tlcManager.PropertyChanged -= TlcManager_PropertyChanged;
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
if (_settingsService is not null)
if (_settings is not null)
{
_settingsService.SettingsChanged -= SettingsChangedHandler;
_settings.SettingsChanged -= SettingsChangedHandler;
}
WeakReferenceMessenger.Default.UnregisterAll(this);

View File

@@ -5,7 +5,6 @@
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;
@@ -13,7 +12,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 ISettingsService _settingsService;
private readonly SettingsModel _settingsModel;
private readonly DockBandSettings _dockSettingsModel;
private readonly TopLevelViewModel _adapter;
private readonly DockBandViewModel? _bandViewModel;
@@ -129,19 +128,19 @@ public partial class DockBandSettingsViewModel : ObservableObject
DockBandSettings dockSettingsModel,
TopLevelViewModel topLevelAdapter,
DockBandViewModel? bandViewModel,
ISettingsService settingsService)
SettingsModel settingsModel)
{
_dockSettingsModel = dockSettingsModel;
_adapter = topLevelAdapter;
_bandViewModel = bandViewModel;
_settingsService = settingsService;
_settingsModel = settingsModel;
_pinSide = FetchPinSide();
_showLabels = FetchShowLabels();
}
private DockPinSide FetchPinSide()
{
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
var inStart = dockSettings.StartBands.Any(b => b.CommandId == _dockSettingsModel.CommandId);
if (inStart)
{
@@ -176,7 +175,7 @@ public partial class DockBandSettingsViewModel : ObservableObject
private void Save()
{
_settingsService.Save();
SettingsModel.SaveSettings(_settingsModel);
}
private void UpdatePinSide(DockPinSide value)
@@ -189,7 +188,7 @@ public partial class DockBandSettingsViewModel : ObservableObject
public void SetBandPosition(DockPinSide side, int? index)
{
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
// Remove from all sides first
dockSettings.StartBands.RemoveAll(b => b.CommandId == _dockSettingsModel.CommandId);

View File

@@ -7,7 +7,6 @@ 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;
@@ -16,7 +15,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Dock;
public sealed partial class DockViewModel
{
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settingsModel;
private readonly DockPageContext _pageContext; // only to be used for our own context menu - not for dock bands themselves
private readonly IContextMenuFactory _contextMenuFactory;
@@ -35,13 +34,13 @@ public sealed partial class DockViewModel
public DockViewModel(
TopLevelCommandManager tlcManager,
IContextMenuFactory contextMenuFactory,
TaskScheduler scheduler,
ISettingsService settingsService)
SettingsModel settings,
TaskScheduler scheduler)
{
_topLevelCommandManager = tlcManager;
_contextMenuFactory = contextMenuFactory;
_settingsService = settingsService;
_settings = _settingsService.Settings.DockSettings;
_settingsModel = settings;
_settings = settings.DockSettings;
Scheduler = scheduler;
_pageContext = new(this);
@@ -149,7 +148,7 @@ public sealed partial class DockViewModel
private void SaveSettings()
{
_settingsService.Save();
SettingsModel.SaveSettings(_settingsModel);
}
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
@@ -194,7 +193,7 @@ public sealed partial class DockViewModel
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
{
var bandId = band.Id;
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
@@ -229,7 +228,7 @@ public sealed partial class DockViewModel
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
{
var bandId = band.Id;
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
@@ -302,7 +301,7 @@ public sealed partial class DockViewModel
_snapshotCenterBands = null;
_snapshotEndBands = null;
_snapshotBandViewModels = null;
_settingsService.Save();
SettingsModel.SaveSettings(_settingsModel);
Logger.LogDebug("Saved band order to settings");
}
@@ -317,7 +316,7 @@ public sealed partial class DockViewModel
/// </summary>
public void SnapshotBandOrder()
{
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.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();
@@ -359,7 +358,7 @@ public sealed partial class DockViewModel
band.RestoreShowLabels();
}
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
// Restore settings from snapshot
dockSettings.StartBands.Clear();
@@ -401,7 +400,7 @@ public sealed partial class DockViewModel
return;
}
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
StartItems.Clear();
CenterItems.Clear();
@@ -434,7 +433,7 @@ public sealed partial class DockViewModel
private void RebuildUICollections()
{
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
// Create a lookup of all current band ViewModels
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
@@ -511,7 +510,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 = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
// Create the band view model
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
@@ -551,7 +550,7 @@ public sealed partial class DockViewModel
public void UnpinBand(DockBandViewModel band)
{
var bandId = band.Id;
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settingsModel.DockSettings;
// Remove from settings
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
@@ -617,7 +616,7 @@ public sealed partial class DockViewModel
private void EmitDockConfiguration()
{
var isDockEnabled = _settingsService.Settings.EnableDock;
var isDockEnabled = _settingsModel.EnableDock;
var dockSide = isDockEnabled ? _settings.Side.ToString().ToLowerInvariant() : "none";
static string FormatBands(List<DockBandSettings> bands) =>

View File

@@ -23,7 +23,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
/// </summary>
public sealed partial class DockAppearanceSettingsViewModel : ObservableObject, IDisposable
{
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settings;
private readonly DockSettings _dockSettings;
private readonly UISettings _uiSettings;
private readonly IThemeService _themeService;
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
@@ -36,18 +37,18 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public int ThemeIndex
{
get => (int)_settingsService.Settings.DockSettings.Theme;
get => (int)_dockSettings.Theme;
set => Theme = (UserTheme)value;
}
public UserTheme Theme
{
get => _settingsService.Settings.DockSettings.Theme;
get => _dockSettings.Theme;
set
{
if (_settingsService.Settings.DockSettings.Theme != value)
if (_dockSettings.Theme != value)
{
_settingsService.Settings.DockSettings.Theme = value;
_dockSettings.Theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ThemeIndex));
Save();
@@ -57,18 +58,18 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public int BackdropIndex
{
get => (int)_settingsService.Settings.DockSettings.Backdrop;
get => (int)_dockSettings.Backdrop;
set => Backdrop = (DockBackdrop)value;
}
public DockBackdrop Backdrop
{
get => _settingsService.Settings.DockSettings.Backdrop;
get => _dockSettings.Backdrop;
set
{
if (_settingsService.Settings.DockSettings.Backdrop != value)
if (_dockSettings.Backdrop != value)
{
_settingsService.Settings.DockSettings.Backdrop = value;
_dockSettings.Backdrop = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BackdropIndex));
Save();
@@ -78,12 +79,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public ColorizationMode ColorizationMode
{
get => _settingsService.Settings.DockSettings.ColorizationMode;
get => _dockSettings.ColorizationMode;
set
{
if (_settingsService.Settings.DockSettings.ColorizationMode != value)
if (_dockSettings.ColorizationMode != value)
{
_settingsService.Settings.DockSettings.ColorizationMode = value;
_dockSettings.ColorizationMode = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorizationModeIndex));
OnPropertyChanged(nameof(IsCustomTintVisible));
@@ -106,18 +107,18 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public int ColorizationModeIndex
{
get => (int)_settingsService.Settings.DockSettings.ColorizationMode;
get => (int)_dockSettings.ColorizationMode;
set => ColorizationMode = (ColorizationMode)value;
}
public Color ThemeColor
{
get => _settingsService.Settings.DockSettings.CustomThemeColor;
get => _dockSettings.CustomThemeColor;
set
{
if (_settingsService.Settings.DockSettings.CustomThemeColor != value)
if (_dockSettings.CustomThemeColor != value)
{
_settingsService.Settings.DockSettings.CustomThemeColor = value;
_dockSettings.CustomThemeColor = value;
OnPropertyChanged();
@@ -133,10 +134,10 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public int ColorIntensity
{
get => _settingsService.Settings.DockSettings.CustomThemeColorIntensity;
get => _dockSettings.CustomThemeColorIntensity;
set
{
_settingsService.Settings.DockSettings.CustomThemeColorIntensity = value;
_dockSettings.CustomThemeColorIntensity = value;
OnPropertyChanged();
Save();
}
@@ -144,12 +145,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public string BackgroundImagePath
{
get => _settingsService.Settings.DockSettings.BackgroundImagePath ?? string.Empty;
get => _dockSettings.BackgroundImagePath ?? string.Empty;
set
{
if (_settingsService.Settings.DockSettings.BackgroundImagePath != value)
if (_dockSettings.BackgroundImagePath != value)
{
_settingsService.Settings.DockSettings.BackgroundImagePath = value;
_dockSettings.BackgroundImagePath = value;
OnPropertyChanged();
if (BackgroundImageOpacity == 0)
@@ -164,12 +165,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public int BackgroundImageOpacity
{
get => _settingsService.Settings.DockSettings.BackgroundImageOpacity;
get => _dockSettings.BackgroundImageOpacity;
set
{
if (_settingsService.Settings.DockSettings.BackgroundImageOpacity != value)
if (_dockSettings.BackgroundImageOpacity != value)
{
_settingsService.Settings.DockSettings.BackgroundImageOpacity = value;
_dockSettings.BackgroundImageOpacity = value;
OnPropertyChanged();
Save();
}
@@ -178,12 +179,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public int BackgroundImageBrightness
{
get => _settingsService.Settings.DockSettings.BackgroundImageBrightness;
get => _dockSettings.BackgroundImageBrightness;
set
{
if (_settingsService.Settings.DockSettings.BackgroundImageBrightness != value)
if (_dockSettings.BackgroundImageBrightness != value)
{
_settingsService.Settings.DockSettings.BackgroundImageBrightness = value;
_dockSettings.BackgroundImageBrightness = value;
OnPropertyChanged();
Save();
}
@@ -192,12 +193,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public int BackgroundImageBlurAmount
{
get => _settingsService.Settings.DockSettings.BackgroundImageBlurAmount;
get => _dockSettings.BackgroundImageBlurAmount;
set
{
if (_settingsService.Settings.DockSettings.BackgroundImageBlurAmount != value)
if (_dockSettings.BackgroundImageBlurAmount != value)
{
_settingsService.Settings.DockSettings.BackgroundImageBlurAmount = value;
_dockSettings.BackgroundImageBlurAmount = value;
OnPropertyChanged();
Save();
}
@@ -206,12 +207,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
public BackgroundImageFit BackgroundImageFit
{
get => _settingsService.Settings.DockSettings.BackgroundImageFit;
get => _dockSettings.BackgroundImageFit;
set
{
if (_settingsService.Settings.DockSettings.BackgroundImageFit != value)
if (_dockSettings.BackgroundImageFit != value)
{
_settingsService.Settings.DockSettings.BackgroundImageFit = value;
_dockSettings.BackgroundImageFit = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BackgroundImageFitIndex));
Save();
@@ -236,15 +237,15 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
[ObservableProperty]
public partial bool IsColorizationDetailsExpanded { get; set; }
public bool IsCustomTintVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintIntensityVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
public bool IsCustomTintIntensityVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
public bool IsBackgroundControlsVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.Image;
public bool IsBackgroundControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.Image;
public bool IsNoBackgroundVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.None;
public bool IsNoBackgroundVisible => _dockSettings.ColorizationMode is ColorizationMode.None;
public bool IsAccentColorControlsVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public bool IsAccentColorControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
@@ -267,11 +268,12 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
public DockAppearanceSettingsViewModel(IThemeService themeService, ISettingsService settingsService)
public DockAppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
{
_themeService = themeService;
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
_settingsService = settingsService;
_settings = settings;
_dockSettings = settings.DockSettings;
_uiSettings = new UISettings();
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
@@ -279,7 +281,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
Reapply();
IsColorizationDetailsExpanded = _settingsService.Settings.DockSettings.ColorizationMode != ColorizationMode.None;
IsColorizationDetailsExpanded = _dockSettings.ColorizationMode != ColorizationMode.None;
}
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
@@ -300,7 +302,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
private void Save()
{
_settingsService.Save();
SettingsModel.SaveSettings(_settings);
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}

View File

@@ -5,13 +5,12 @@
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 ISettingsService _settingsService;
private readonly SettingsModel _settings;
private readonly FallbackSettings _fallbackSettings;
public string DisplayName { get; private set; } = string.Empty;
@@ -63,10 +62,10 @@ public partial class FallbackSettingsViewModel : ObservableObject
public FallbackSettingsViewModel(
TopLevelViewModel fallback,
FallbackSettings fallbackSettings,
ProviderSettingsViewModel providerSettings,
ISettingsService settingsService)
SettingsModel settingsModel,
ProviderSettingsViewModel providerSettings)
{
_settingsService = settingsService;
_settings = settingsModel;
_fallbackSettings = fallbackSettings;
Id = fallback.Id;
@@ -80,7 +79,7 @@ public partial class FallbackSettingsViewModel : ObservableObject
private void Save()
{
_settingsService.Save();
SettingsModel.SaveSettings(_settings);
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
}
}

View File

@@ -3,7 +3,6 @@
// 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;
@@ -13,10 +12,10 @@ public partial class HotkeyManager : ObservableObject
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly List<TopLevelHotkey> _commandHotkeys;
public HotkeyManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
public HotkeyManager(TopLevelCommandManager tlcManager, SettingsModel settings)
{
_topLevelCommandManager = tlcManager;
_commandHotkeys = settingsService.Settings.CommandHotkeys;
_commandHotkeys = settings.CommandHotkeys;
}
public void UpdateHotkey(string commandId, HotkeySettings? hotkey)

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -10,7 +10,6 @@ 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;
@@ -23,7 +22,7 @@ public partial class ProviderSettingsViewModel : ObservableObject
private readonly CommandProviderWrapper _provider;
private readonly ProviderSettings _providerSettings;
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settings;
private readonly Lock _initializeSettingsLock = new();
private Task? _initializeSettingsTask;
@@ -31,11 +30,11 @@ public partial class ProviderSettingsViewModel : ObservableObject
public ProviderSettingsViewModel(
CommandProviderWrapper provider,
ProviderSettings providerSettings,
ISettingsService settingsService)
SettingsModel settings)
{
_provider = provider;
_providerSettings = providerSettings;
_settingsService = settingsService;
_settings = settings;
LoadingSettings = _provider.Settings?.HasSettings ?? false;
@@ -180,18 +179,18 @@ public partial class ProviderSettingsViewModel : ObservableObject
{
if (_providerSettings.FallbackCommands.TryGetValue(fallbackItem.Id, out var fallbackSettings))
{
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, fallbackSettings, this, _settingsService));
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, fallbackSettings, _settings, this));
}
else
{
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, new(), this, _settingsService));
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, new(), _settings, this));
}
}
FallbackCommands = fallbackViewModels;
}
private void Save() => _settingsService.Save();
private void Save() => SettingsModel.SaveSettings(_settings);
private void InitializeSettingsPage()
{

View File

@@ -1,46 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using 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");
}
}

View File

@@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using 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;
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.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);
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using 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;
}

View File

@@ -1,79 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.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);
}
}
}

View File

@@ -1,122 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.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;
}
}

View File

@@ -3,6 +3,7 @@
// 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;
@@ -28,7 +29,7 @@ public class DockSettings
public ColorizationMode ColorizationMode { get; set; }
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 Color CustomThemeColor { get; set; } = Colors.Transparent;
public int CustomThemeColorIntensity { get; set; } = 100;

View File

@@ -2,15 +2,30 @@
// 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
@@ -62,7 +77,7 @@ public partial class SettingsModel : ObservableObject
public ColorizationMode ColorizationMode { get; set; }
public Color CustomThemeColor { get; set; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent
public Color CustomThemeColor { get; set; } = Colors.Transparent;
public int CustomThemeColorIntensity { get; set; } = 100;
@@ -87,6 +102,11 @@ public partial class SettingsModel : ObservableObject
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
static SettingsModel()
{
FilePath = SettingsJsonPath();
}
public ProviderSettings GetProviderSettings(CommandProviderWrapper provider)
{
ProviderSettings? settings;
@@ -123,6 +143,165 @@ 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()
// {

View File

@@ -27,7 +27,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
TimeSpan.FromSeconds(180),
];
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settings;
private readonly TopLevelCommandManager _topLevelCommandManager;
public event PropertyChangedEventHandler? PropertyChanged;
@@ -38,10 +38,10 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public HotkeySettings? Hotkey
{
get => _settingsService.Settings.Hotkey;
get => _settings.Hotkey;
set
{
_settingsService.Settings.Hotkey = value ?? SettingsModel.DefaultActivationShortcut;
_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 => _settingsService.Settings.UseLowLevelGlobalHotkey;
get => _settings.UseLowLevelGlobalHotkey;
set
{
_settingsService.Settings.UseLowLevelGlobalHotkey = value;
_settings.UseLowLevelGlobalHotkey = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
Save();
}
@@ -60,100 +60,100 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public bool AllowExternalReload
{
get => _settingsService.Settings.AllowExternalReload;
get => _settings.AllowExternalReload;
set
{
_settingsService.Settings.AllowExternalReload = value;
_settings.AllowExternalReload = value;
Save();
}
}
public bool ShowAppDetails
{
get => _settingsService.Settings.ShowAppDetails;
get => _settings.ShowAppDetails;
set
{
_settingsService.Settings.ShowAppDetails = value;
_settings.ShowAppDetails = value;
Save();
}
}
public bool BackspaceGoesBack
{
get => _settingsService.Settings.BackspaceGoesBack;
get => _settings.BackspaceGoesBack;
set
{
_settingsService.Settings.BackspaceGoesBack = value;
_settings.BackspaceGoesBack = value;
Save();
}
}
public bool SingleClickActivates
{
get => _settingsService.Settings.SingleClickActivates;
get => _settings.SingleClickActivates;
set
{
_settingsService.Settings.SingleClickActivates = value;
_settings.SingleClickActivates = value;
Save();
}
}
public bool HighlightSearchOnActivate
{
get => _settingsService.Settings.HighlightSearchOnActivate;
get => _settings.HighlightSearchOnActivate;
set
{
_settingsService.Settings.HighlightSearchOnActivate = value;
_settings.HighlightSearchOnActivate = value;
Save();
}
}
public bool KeepPreviousQuery
{
get => _settingsService.Settings.KeepPreviousQuery;
get => _settings.KeepPreviousQuery;
set
{
_settingsService.Settings.KeepPreviousQuery = value;
_settings.KeepPreviousQuery = value;
Save();
}
}
public int MonitorPositionIndex
{
get => (int)_settingsService.Settings.SummonOn;
get => (int)_settings.SummonOn;
set
{
_settingsService.Settings.SummonOn = (MonitorBehavior)value;
_settings.SummonOn = (MonitorBehavior)value;
Save();
}
}
public bool ShowSystemTrayIcon
{
get => _settingsService.Settings.ShowSystemTrayIcon;
get => _settings.ShowSystemTrayIcon;
set
{
_settingsService.Settings.ShowSystemTrayIcon = value;
_settings.ShowSystemTrayIcon = value;
Save();
}
}
public bool IgnoreShortcutWhenFullscreen
{
get => _settingsService.Settings.IgnoreShortcutWhenFullscreen;
get => _settings.IgnoreShortcutWhenFullscreen;
set
{
_settingsService.Settings.IgnoreShortcutWhenFullscreen = value;
_settings.IgnoreShortcutWhenFullscreen = value;
Save();
}
}
public bool DisableAnimations
{
get => _settingsService.Settings.DisableAnimations;
get => _settings.DisableAnimations;
set
{
_settingsService.Settings.DisableAnimations = value;
_settings.DisableAnimations = value;
Save();
}
}
@@ -162,7 +162,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
{
get
{
var index = AutoGoHomeIntervals.IndexOf(_settingsService.Settings.AutoGoHomeInterval);
var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval);
return index >= 0 ? index : 0;
}
@@ -170,7 +170,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
{
if (value >= 0 && value < AutoGoHomeIntervals.Count)
{
_settingsService.Settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
_settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
}
Save();
@@ -179,60 +179,60 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public int EscapeKeyBehaviorIndex
{
get => (int)_settingsService.Settings.EscapeKeyBehaviorSetting;
get => (int)_settings.EscapeKeyBehaviorSetting;
set
{
_settingsService.Settings.EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value;
_settings.EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value;
Save();
}
}
public DockSide Dock_Side
{
get => _settingsService.Settings.DockSettings.Side;
get => _settings.DockSettings.Side;
set
{
_settingsService.Settings.DockSettings.Side = value;
_settings.DockSettings.Side = value;
Save();
}
}
public DockSize Dock_DockSize
{
get => _settingsService.Settings.DockSettings.DockSize;
get => _settings.DockSettings.DockSize;
set
{
_settingsService.Settings.DockSettings.DockSize = value;
_settings.DockSettings.DockSize = value;
Save();
}
}
public DockBackdrop Dock_Backdrop
{
get => _settingsService.Settings.DockSettings.Backdrop;
get => _settings.DockSettings.Backdrop;
set
{
_settingsService.Settings.DockSettings.Backdrop = value;
_settings.DockSettings.Backdrop = value;
Save();
}
}
public bool Dock_ShowLabels
{
get => _settingsService.Settings.DockSettings.ShowLabels;
get => _settings.DockSettings.ShowLabels;
set
{
_settingsService.Settings.DockSettings.ShowLabels = value;
_settings.DockSettings.ShowLabels = value;
Save();
}
}
public bool EnableDock
{
get => _settingsService.Settings.EnableDock;
get => _settings.EnableDock;
set
{
_settingsService.Settings.EnableDock = value;
_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(TopLevelCommandManager topLevelCommandManager, TaskScheduler scheduler, IThemeService themeService, ISettingsService settingsService)
public SettingsViewModel(SettingsModel settings, TopLevelCommandManager topLevelCommandManager, TaskScheduler scheduler, IThemeService themeService)
{
_settingsService = settingsService;
_settings = settings;
_topLevelCommandManager = topLevelCommandManager;
Appearance = new AppearanceSettingsViewModel(themeService, settingsService);
DockAppearance = new DockAppearanceSettingsViewModel(themeService, settingsService);
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
var activeProviders = GetCommandProviders();
var allProviderSettings = _settingsService.Settings.ProviderSettings;
var allProviderSettings = _settings.ProviderSettings;
var fallbacks = new List<FallbackSettingsViewModel>();
var currentRankings = _settingsService.Settings.FallbackRanks;
var currentRankings = _settings.FallbackRanks;
var needsSave = false;
foreach (var item in activeProviders)
{
var providerSettings = _settingsService.Settings.GetProviderSettings(item);
var providerSettings = settings.GetProviderSettings(item);
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, settingsService);
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _settings);
CommandProviders.Add(settingsModel);
fallbacks.AddRange(settingsModel.FallbackCommands);
@@ -306,10 +306,10 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public void ApplyFallbackSort()
{
_settingsService.Settings.FallbackRanks = FallbackRankings.Select(s => s.Id).ToArray();
_settings.FallbackRanks = FallbackRankings.Select(s => s.Id).ToArray();
Save();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackRankings)));
}
private void Save() => _settingsService.Save();
private void Save() => SettingsModel.SaveSettings(_settings);
}

View File

@@ -467,16 +467,6 @@ 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;
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -9,7 +9,6 @@ using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Text;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -22,7 +21,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider, IPrecomputedListItem
{
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settings;
private readonly ProviderSettings _providerSettings;
private readonly IServiceProvider _serviceProvider;
private readonly CommandItemViewModel _commandItemViewModel;
@@ -186,9 +185,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
return null;
}
var bandSettings = _settingsService.Settings.DockSettings.StartBands
.Concat(_settingsService.Settings.DockSettings.CenterBands)
.Concat(_settingsService.Settings.DockSettings.EndBands)
var bandSettings = _settings.DockSettings.StartBands
.Concat(_settings.DockSettings.CenterBands)
.Concat(_settings.DockSettings.EndBands)
.FirstOrDefault(band => band.CommandId == this.Id);
if (bandSettings is null)
{
@@ -209,13 +208,14 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
TopLevelType topLevelType,
CommandPaletteHost extensionHost,
ICommandProviderContext commandProviderContext,
SettingsModel settings,
ProviderSettings providerSettings,
IServiceProvider serviceProvider,
ICommandItem? commandItem,
IContextMenuFactory? contextMenuFactory)
{
_serviceProvider = serviceProvider;
_settingsService = serviceProvider.GetRequiredService<ISettingsService>();
_settings = settings;
_providerSettings = providerSettings;
ProviderContext = commandProviderContext;
_commandItemViewModel = item;
@@ -313,7 +313,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
}
}
private void Save() => _settingsService.Save();
private void Save() => SettingsModel.SaveSettings(_settings);
private void HandleChangeAlias()
{
@@ -347,7 +347,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
private void UpdateHotkey()
{
var hotkey = _settingsService.Settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault();
var hotkey = _settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault();
if (hotkey is not null)
{
_hotkey = hotkey.Hotkey;

View File

@@ -183,10 +183,11 @@ public partial class App : Application, IDisposable
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)
{
// Models & persistence services
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
// Models
var sm = SettingsModel.LoadSettings();
services.AddSingleton(sm);
var state = AppStateModel.LoadState();
services.AddSingleton(state);
// Services
services.AddSingleton<ICommandProviderCache, DefaultCommandProviderCache>();

View File

@@ -6,7 +6,6 @@ using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -16,12 +15,12 @@ namespace Microsoft.CmdPal.UI;
internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFactory
{
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settingsModel;
private readonly TopLevelCommandManager _topLevelCommandManager;
public CommandPaletteContextMenuFactory(ISettingsService settingsService, TopLevelCommandManager topLevelCommandManager)
public CommandPaletteContextMenuFactory(SettingsModel settingsModel, TopLevelCommandManager topLevelCommandManager)
{
_settingsService = settingsService;
_settingsModel = settingsModel;
_topLevelCommandManager = topLevelCommandManager;
}
@@ -66,7 +65,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
var providerId = providerContext.ProviderId;
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
{
var providerSettings = _settingsService.Settings.GetProviderSettings(provider);
var providerSettings = _settingsModel.GetProviderSettings(provider);
var alreadyPinnedToTopLevel = providerSettings.PinnedCommandIds.Contains(itemId);
@@ -83,7 +82,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
providerId: providerId,
pin: !alreadyPinnedToTopLevel,
PinLocation.TopLevel,
_settingsService,
_settingsModel,
_topLevelCommandManager);
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
@@ -133,7 +132,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
var providerId = providerContext.ProviderId;
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
{
var providerSettings = _settingsService.Settings.GetProviderSettings(provider);
var providerSettings = _settingsModel.GetProviderSettings(provider);
var isPinnedSubCommand = providerSettings.PinnedCommandIds.Contains(itemId);
if (isPinnedSubCommand)
@@ -143,7 +142,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
providerId: providerId,
pin: !isPinnedSubCommand,
PinLocation.TopLevel,
_settingsService,
_settingsModel,
_topLevelCommandManager);
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
@@ -169,22 +168,22 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
List<IContextItem> moreCommands,
CommandItemViewModel commandItem)
{
if (!_settingsService.Settings.EnableDock)
if (!_settingsModel.EnableDock)
{
return;
}
var inStartBands = _settingsService.Settings.DockSettings.StartBands.Any(band => MatchesBand(band, itemId, providerId));
var inCenterBands = _settingsService.Settings.DockSettings.CenterBands.Any(band => MatchesBand(band, itemId, providerId));
var inEndBands = _settingsService.Settings.DockSettings.EndBands.Any(band => MatchesBand(band, itemId, providerId));
var inStartBands = _settingsModel.DockSettings.StartBands.Any(band => MatchesBand(band, itemId, providerId));
var inCenterBands = _settingsModel.DockSettings.CenterBands.Any(band => MatchesBand(band, itemId, providerId));
var inEndBands = _settingsModel.DockSettings.EndBands.Any(band => MatchesBand(band, itemId, providerId));
var alreadyPinned = inStartBands || inCenterBands || inEndBands; /** &&
_settingsService.Settings.DockSettings.PinnedCommands.Contains(this.Id)**/
_settingsModel.DockSettings.PinnedCommands.Contains(this.Id)**/
var pinToTopLevelCommand = new PinToCommand(
commandId: itemId,
providerId: providerId,
pin: !alreadyPinned,
PinLocation.Dock,
_settingsService,
_settingsModel,
_topLevelCommandManager);
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
@@ -232,7 +231,7 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
{
private readonly string _commandId;
private readonly string _providerId;
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settings;
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly bool _pin;
private readonly PinLocation _pinLocation;
@@ -252,13 +251,13 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
string providerId,
bool pin,
PinLocation pinLocation,
ISettingsService settingsService,
SettingsModel settings,
TopLevelCommandManager topLevelCommandManager)
{
_commandId = commandId;
_providerId = providerId;
_pinLocation = pinLocation;
_settingsService = settingsService;
_settings = settings;
_topLevelCommandManager = topLevelCommandManager;
_pin = pin;
}

View File

@@ -18,10 +18,10 @@ public sealed partial class FallbackRanker : UserControl
{
this.InitializeComponent();
var settings = App.Current.Services.GetService<SettingsModel>()!;
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var themeService = App.Current.Services.GetService<IThemeService>()!;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
}
private void ListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)

View File

@@ -9,7 +9,6 @@ using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.Views;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Dispatching;
@@ -50,7 +49,7 @@ public sealed partial class SearchBar : UserControl,
// 0.6+ suggestions
private string? _textToSuggest;
private SettingsModel Settings => App.Current.Services.GetRequiredService<ISettingsService>().Settings;
private SettingsModel Settings => App.Current.Services.GetRequiredService<SettingsModel>();
public PageViewModel? CurrentPageViewModel
{

View File

@@ -46,7 +46,6 @@ public sealed partial class DockWindow : WindowEx,
#pragma warning restore SA1306 // Field names should begin with lower-case letter
private readonly IThemeService _themeService;
private readonly ISettingsService _settingsService;
private readonly DockWindowViewModel _windowViewModel;
private readonly HiddenOwnerWindowBehavior _hiddenOwnerWindowBehavior = new();
@@ -69,9 +68,8 @@ public sealed partial class DockWindow : WindowEx,
public DockWindow()
{
var serviceProvider = App.Current.Services;
var mainSettings = serviceProvider.GetRequiredService<ISettingsService>().Settings;
_settingsService = serviceProvider.GetRequiredService<ISettingsService>();
_settingsService.SettingsChanged += SettingsChangedHandler;
var mainSettings = serviceProvider.GetService<SettingsModel>()!;
mainSettings.SettingsChanged += SettingsChangedHandler;
_settings = mainSettings.DockSettings;
_lastSize = _settings.DockSize;
@@ -130,9 +128,9 @@ public sealed partial class DockWindow : WindowEx,
UpdateSettingsOnUiThread();
}
private void SettingsChangedHandler(ISettingsService sender, SettingsModel args)
private void SettingsChangedHandler(SettingsModel sender, object? args)
{
_settings = args.DockSettings;
_settings = sender.DockSettings;
DispatcherQueue.TryEnqueue(UpdateSettingsOnUiThread);
}
@@ -623,7 +621,9 @@ public sealed partial class DockWindow : WindowEx,
private void DockWindow_Closed(object sender, WindowEventArgs args)
{
_settingsService.SettingsChanged -= SettingsChangedHandler;
var serviceProvider = App.Current.Services;
var settings = serviceProvider.GetService<SettingsModel>();
settings?.SettingsChanged -= SettingsChangedHandler;
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
DisposeAcrylic();

View File

@@ -9,7 +9,6 @@ using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
@@ -184,7 +183,7 @@ public sealed partial class ListPage : Page,
return;
}
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settings = App.Current.Services.GetService<SettingsModel>()!;
if (settings.SingleClickActivates)
{
ViewModel?.InvokeItemCommand.Execute(item);
@@ -204,7 +203,7 @@ public sealed partial class ListPage : Page,
{
if (ItemView.SelectedItem is ListItemViewModel vm)
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settings = App.Current.Services.GetService<SettingsModel>()!;
if (!settings.SingleClickActivates)
{
ViewModel?.InvokeItemCommand.Execute(vm);

View File

@@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.UI.Xaml;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -26,7 +25,7 @@ internal sealed partial class TrayIconService
private const uint MY_NOTIFY_ID = 1000;
private const uint WM_TRAY_ICON = PInvoke.WM_USER + 1;
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settingsModel;
private readonly uint WM_TASKBAR_RESTART;
private Window? _window;
@@ -37,9 +36,9 @@ internal sealed partial class TrayIconService
private DestroyIconSafeHandle? _largeIcon;
private DestroyMenuSafeHandle? _popupMenu;
public TrayIconService(ISettingsService settingsService)
public TrayIconService(SettingsModel settingsModel)
{
_settingsService = settingsService;
_settingsModel = settingsModel;
// TaskbarCreated is the message that's broadcast when explorer.exe
// restarts. We need to know when that happens to be able to bring our
@@ -49,7 +48,7 @@ internal sealed partial class TrayIconService
public void SetupTrayIcon(bool? showSystemTrayIcon = null)
{
if (showSystemTrayIcon ?? _settingsService.Settings.ShowSystemTrayIcon)
if (showSystemTrayIcon ?? _settingsModel.ShowSystemTrayIcon)
{
if (_window is null)
{

View File

@@ -94,7 +94,6 @@ public sealed partial class MainWindow : WindowEx,
private WindowPosition _currentWindowPosition = new();
private bool _preventHideWhenDeactivated;
private bool _isLoadedFromDock;
private DevRibbon? _devRibbon;
@@ -143,7 +142,7 @@ public sealed partial class MainWindow : WindowEx,
this.SetIcon();
AppWindow.Title = RS_.GetString("AppName");
RestoreWindowPositionFromSavedSettings();
RestoreWindowPosition();
UpdateWindowPositionInMemory();
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
@@ -170,7 +169,7 @@ public sealed partial class MainWindow : WindowEx,
// Load our settings, and then also wire up a settings changed handler
HotReloadSettings();
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged += SettingsChangedHandler;
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
// Make sure that we update the acrylic theme when the OS theme changes
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateBackdrop);
@@ -211,7 +210,7 @@ public sealed partial class MainWindow : WindowEx,
}
}
private void SettingsChangedHandler(ISettingsService sender, SettingsModel args)
private void SettingsChangedHandler(SettingsModel sender, object? args)
{
DispatcherQueue.TryEnqueue(HotReloadSettings);
}
@@ -246,26 +245,10 @@ public sealed partial class MainWindow : WindowEx,
private void PositionCentered(DisplayArea displayArea)
{
// Use the saved window size when available so that a dock-resized HWND
// (hidden but not destroyed) doesn't dictate the size on normal reopen.
SizeInt32 windowSize;
int windowDpi;
if (_currentWindowPosition.IsSizeValid)
{
windowSize = new SizeInt32(_currentWindowPosition.Width, _currentWindowPosition.Height);
windowDpi = _currentWindowPosition.Dpi;
}
else
{
windowSize = AppWindow.Size;
windowDpi = (int)this.GetDpiForWindow();
}
var rect = WindowPositionHelper.CenterOnDisplay(
displayArea,
windowSize,
windowDpi);
displayArea,
AppWindow.Size,
(int)this.GetDpiForWindow());
if (rect is not null)
{
@@ -273,9 +256,10 @@ public sealed partial class MainWindow : WindowEx,
}
}
private void RestoreWindowPosition(WindowPosition? savedPosition)
private void RestoreWindowPosition()
{
if (savedPosition?.IsSizeValid != true)
var settings = App.Current.Services.GetService<SettingsModel>();
if (settings?.LastWindowPosition is not { Width: > 0, Height: > 0 } savedPosition)
{
// don't try to restore if the saved position is invalid, just recenter
PositionCentered();
@@ -290,17 +274,6 @@ public sealed partial class MainWindow : WindowEx,
MoveAndResizeDpiAware(newRect);
}
private void RestoreWindowPositionFromSavedSettings()
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
RestoreWindowPosition(settings?.LastWindowPosition);
}
private void RestoreWindowPositionFromMemory()
{
RestoreWindowPosition(_currentWindowPosition);
}
/// <summary>
/// Moves and resizes the window while suppressing WM_DPICHANGED.
/// The caller is expected to provide a rect already scaled for the target display's DPI.
@@ -363,7 +336,7 @@ public sealed partial class MainWindow : WindowEx,
private void HotReloadSettings()
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settings = App.Current.Services.GetService<SettingsModel>()!;
SetupHotkey(settings);
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
@@ -705,9 +678,7 @@ public sealed partial class MainWindow : WindowEx,
public void Receive(ShowWindowMessage message)
{
_isLoadedFromDock = false;
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settings = App.Current.Services.GetService<SettingsModel>()!;
// Start session tracking
_sessionStopwatch = Stopwatch.StartNew();
@@ -719,13 +690,6 @@ public sealed partial class MainWindow : WindowEx,
internal void Receive(ShowPaletteAtMessage message)
{
_isLoadedFromDock = true;
// Reset the size in case users have resized a dock window.
// Ideally in the future, we'll have defined sizes that opening
// a dock window will adhere to, but alas, that's the future.
RestoreWindowPositionFromMemory();
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
}
@@ -896,22 +860,16 @@ public sealed partial class MainWindow : WindowEx,
internal void MainWindow_Closed(object sender, WindowEventArgs args)
{
var serviceProvider = App.Current.Services;
UpdateWindowPositionInMemory();
if (!_isLoadedFromDock)
{
UpdateWindowPositionInMemory();
}
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var settings = serviceProvider.GetService<SettingsModel>();
if (settings is not null)
{
// If we were last shown from the dock, _currentWindowPosition still holds
// the last non-dock placement because dock sessions intentionally skip updates.
// a quick sanity check, so we don't overwrite correct values
if (_currentWindowPosition.IsSizeValid)
{
settings.LastWindowPosition = _currentWindowPosition;
settingsService.Save();
SettingsModel.SaveSettings(settings);
}
}
@@ -1002,11 +960,7 @@ public sealed partial class MainWindow : WindowEx,
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
// Save the current window position before hiding the window
// but not when opened from dock — preserve the pre-dock size.
if (!_isLoadedFromDock)
{
UpdateWindowPositionInMemory();
}
UpdateWindowPositionInMemory();
// If there's a debugger attached...
if (System.Diagnostics.Debugger.IsAttached)
@@ -1081,7 +1035,7 @@ public sealed partial class MainWindow : WindowEx,
}
else if (uri.StartsWith("x-cmdpal://reload", StringComparison.OrdinalIgnoreCase))
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settings = App.Current.Services.GetService<SettingsModel>();
if (settings?.AllowExternalReload == true)
{
Logger.LogInfo("External Reload triggered");

View File

@@ -16,7 +16,6 @@ using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.Settings;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
@@ -51,7 +50,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
IRecipient<ShowToastMessage>,
IRecipient<NavigateToPageMessage>,
IRecipient<ShowHideDockMessage>,
IRecipient<GoToPageResultMessage>,
INotifyPropertyChanged,
IDisposable
{
@@ -101,7 +99,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Register<ShowConfirmationMessage>(this);
WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<GoToPageResultMessage>(this);
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
@@ -114,7 +111,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
if (App.Current.Services.GetRequiredService<ISettingsService>().Settings.EnableDock)
if (App.Current.Services.GetService<SettingsModel>()!.EnableDock)
{
_dockWindow = new DockWindow();
_dockWindow.Show();
@@ -128,14 +125,14 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
get
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settings = App.Current.Services.GetService<SettingsModel>()!;
return settings.DisableAnimations ? _noAnimation : _slideRightTransition;
}
}
public void Receive(NavigateBackMessage message)
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settings = App.Current.Services.GetService<SettingsModel>()!;
if (RootFrame.CanGoBack)
{
@@ -204,21 +201,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
});
}
public void Receive(GoToPageResultMessage message)
{
DispatcherQueue.TryEnqueue(() =>
{
try
{
HandleGoToPageOnUiThread(message.Args);
}
catch (Exception ex)
{
Logger.LogError(ex.ToString());
}
});
}
public void Receive(ShowToastMessage message)
{
DispatcherQueue.TryEnqueue(() =>
@@ -282,55 +264,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
private void InitializeConfirmationDialog(ConfirmResultViewModel vm) => vm.SafeInitializePropertiesSynchronous();
// This gets called from the UI thread
private void HandleGoToPageOnUiThread(IGoToPageArgs? args)
{
if (args is null)
{
return;
}
var pageId = args.PageId;
var navigationMode = args.NavigationMode;
if (string.IsNullOrEmpty(pageId))
{
Logger.LogWarning("GoToPage: PageId is null or empty");
return;
}
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var topLevelCommand = tlcManager.LookupCommand(pageId);
if (topLevelCommand is null)
{
Logger.LogWarning($"GoToPage: Could not find page with ID '{pageId}'");
return;
}
switch (navigationMode)
{
case NavigationMode.GoHome:
GoHome(withAnimation: false, focusSearch: false);
break;
case NavigationMode.GoBack:
if (RootFrame.CanGoBack)
{
GoBack(withAnimation: false, focusSearch: false);
}
break;
case NavigationMode.Push:
default:
break;
}
var msg = topLevelCommand.GetPerformCommandMessage();
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(msg);
}
public void Receive(OpenSettingsMessage message)
{
_ = DispatcherQueue.TryEnqueue(() =>
@@ -429,7 +362,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
private void SummonOnUiThread(HotkeySummonMessage message)
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settings = App.Current.Services.GetService<SettingsModel>()!;
var commandId = message.CommandId;
var isRoot = string.IsNullOrEmpty(commandId);
if (isRoot)

View File

@@ -9,7 +9,6 @@ using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Common.Text;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.MainPage;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using WinRT;
@@ -24,13 +23,13 @@ internal sealed class PowerToysRootPageService : IRootPageService
private IExtensionWrapper? _activeExtension;
private Lazy<MainListPage> _mainListPage;
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, AliasManager aliasManager, IFuzzyMatcherProvider fuzzyMatcherProvider, ISettingsService settingsService, IAppStateService appStateService)
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel, IFuzzyMatcherProvider fuzzyMatcherProvider)
{
_tlcManager = topLevelCommandManager;
_mainListPage = new Lazy<MainListPage>(() =>
{
return new MainListPage(_tlcManager, aliasManager, fuzzyMatcherProvider, settingsService, appStateService);
return new MainListPage(_tlcManager, settings, aliasManager, appStateModel, fuzzyMatcherProvider);
});
}

View File

@@ -4,33 +4,32 @@
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Services;
namespace Microsoft.CmdPal.UI;
internal sealed class RunHistoryService : IRunHistoryService
{
private readonly IAppStateService _appStateService;
private readonly AppStateModel _appStateModel;
public RunHistoryService(IAppStateService appStateService)
public RunHistoryService(AppStateModel appStateModel)
{
_appStateService = appStateService;
_appStateModel = appStateModel;
}
public IReadOnlyList<string> GetRunHistory()
{
if (_appStateService.State.RunHistory.Count == 0)
if (_appStateModel.RunHistory.Count == 0)
{
var history = Microsoft.Terminal.UI.RunHistory.CreateRunHistory();
_appStateService.State.RunHistory.AddRange(history);
_appStateModel.RunHistory.AddRange(history);
}
return _appStateService.State.RunHistory;
return _appStateModel.RunHistory;
}
public void ClearRunHistory()
{
_appStateService.State.RunHistory.Clear();
_appStateModel.RunHistory.Clear();
}
public void AddRunHistoryItem(string item)
@@ -41,11 +40,11 @@ internal sealed class RunHistoryService : IRunHistoryService
return; // Do not add empty or whitespace items
}
_appStateService.State.RunHistory.Remove(item);
_appStateModel.RunHistory.Remove(item);
// Add the item to the front of the history
_appStateService.State.RunHistory.Insert(0, item);
_appStateModel.RunHistory.Insert(0, item);
_appStateService.Save();
AppStateModel.SaveState(_appStateModel);
}
}

View File

@@ -27,8 +27,7 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
private static readonly TimeSpan ReloadDebounceInterval = TimeSpan.FromMilliseconds(500);
private readonly UISettings _uiSettings;
private readonly ISettingsService _settingsService;
private readonly SettingsModel _settings;
private readonly ResourceSwapper _resourceSwapper;
private readonly NormalThemeProvider _normalThemeProvider;
private readonly ColorfulThemeProvider _colorfulThemeProvider;
@@ -77,32 +76,32 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
}
// provider selection
var themeColorIntensity = Math.Clamp(_settingsService.Settings.CustomThemeColorIntensity, 0, 100);
var imageTintIntensity = Math.Clamp(_settingsService.Settings.BackgroundImageTintIntensity, 0, 100);
var effectiveColorIntensity = _settingsService.Settings.ColorizationMode == ColorizationMode.Image
var themeColorIntensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
var imageTintIntensity = Math.Clamp(_settings.BackgroundImageTintIntensity, 0, 100);
var effectiveColorIntensity = _settings.ColorizationMode == ColorizationMode.Image
? imageTintIntensity
: themeColorIntensity;
IThemeProvider provider = UseColorfulProvider(effectiveColorIntensity) ? _colorfulThemeProvider : _normalThemeProvider;
// Calculate values
var tint = _settingsService.Settings.ColorizationMode switch
var tint = _settings.ColorizationMode switch
{
ColorizationMode.CustomColor => _settingsService.Settings.CustomThemeColor,
ColorizationMode.CustomColor => _settings.CustomThemeColor,
ColorizationMode.WindowsAccentColor => _uiSettings.GetColorValue(UIColorType.Accent),
ColorizationMode.Image => _settingsService.Settings.CustomThemeColor,
ColorizationMode.Image => _settings.CustomThemeColor,
_ => Colors.Transparent,
};
var effectiveTheme = GetElementTheme((ElementTheme)_settingsService.Settings.Theme);
var imageSource = _settingsService.Settings.ColorizationMode == ColorizationMode.Image
? LoadImageSafe(_settingsService.Settings.BackgroundImagePath)
var effectiveTheme = GetElementTheme((ElementTheme)_settings.Theme);
var imageSource = _settings.ColorizationMode == ColorizationMode.Image
? LoadImageSafe(_settings.BackgroundImagePath)
: null;
var stretch = _settingsService.Settings.BackgroundImageFit switch
var stretch = _settings.BackgroundImageFit switch
{
BackgroundImageFit.Fill => Stretch.Fill,
_ => Stretch.UniformToFill,
};
var opacity = Math.Clamp(_settingsService.Settings.BackgroundImageOpacity, 0, 100) / 100.0;
var opacity = Math.Clamp(_settings.BackgroundImageOpacity, 0, 100) / 100.0;
// create input and offload to actual theme provider
var context = new ThemeContext
@@ -113,16 +112,16 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
BackgroundImageSource = imageSource,
BackgroundImageStretch = stretch,
BackgroundImageOpacity = opacity,
BackdropStyle = _settingsService.Settings.BackdropStyle,
BackdropOpacity = Math.Clamp(_settingsService.Settings.BackdropOpacity, 0, 100) / 100f,
BackdropStyle = _settings.BackdropStyle,
BackdropOpacity = Math.Clamp(_settings.BackdropOpacity, 0, 100) / 100f,
};
var backdrop = provider.GetBackdropParameters(context);
var blur = _settingsService.Settings.BackgroundImageBlurAmount;
var brightness = _settingsService.Settings.BackgroundImageBrightness;
var blur = _settings.BackgroundImageBlurAmount;
var brightness = _settings.BackgroundImageBrightness;
// Create public snapshot (no provider!)
var hasColorization = effectiveColorIntensity > 0
&& _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
&& _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
var snapshot = new ThemeSnapshot
{
@@ -150,7 +149,7 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
Interlocked.Exchange(ref _currentState, newState);
// Compute DockThemeSnapshot from DockSettings
var dockSettings = _settingsService.Settings.DockSettings;
var dockSettings = _settings.DockSettings;
var dockIntensity = Math.Clamp(dockSettings.CustomThemeColorIntensity, 0, 100);
IThemeProvider dockProvider = dockIntensity > 0 && dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
? _colorfulThemeProvider
@@ -209,8 +208,8 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
private bool UseColorfulProvider(int effectiveColorIntensity)
{
return _settingsService.Settings.ColorizationMode == ColorizationMode.Image
|| (effectiveColorIntensity > 0 && _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor);
return _settings.ColorizationMode == ColorizationMode.Image
|| (effectiveColorIntensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor);
}
private static BitmapImage? LoadImageSafe(string? path)
@@ -242,12 +241,13 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
}
}
public ThemeService(ResourceSwapper resourceSwapper, ISettingsService settingsService)
public ThemeService(SettingsModel settings, ResourceSwapper resourceSwapper)
{
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(resourceSwapper);
_settingsService = settingsService;
_settingsService.SettingsChanged += SettingsOnSettingsChanged;
_settings = settings;
_settings.SettingsChanged += SettingsOnSettingsChanged;
_resourceSwapper = resourceSwapper;
@@ -319,7 +319,7 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
};
}
private void SettingsOnSettingsChanged(ISettingsService sender, SettingsModel args)
private void SettingsOnSettingsChanged(SettingsModel sender, object? args)
{
RequestReload();
}
@@ -339,7 +339,7 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
_disposed = true;
_dispatcherQueueTimer?.Stop();
_uiSettings.ColorValuesChanged -= UiSettings_ColorValuesChanged;
_settingsService.SettingsChanged -= SettingsOnSettingsChanged;
_settings.SettingsChanged -= SettingsOnSettingsChanged;
}
private sealed class InternalThemeState

View File

@@ -31,10 +31,10 @@ public sealed partial class AppearancePage : Page
{
InitializeComponent();
var settings = App.Current.Services.GetService<SettingsModel>()!;
var themeService = App.Current.Services.GetRequiredService<IThemeService>();
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
ViewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
ViewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
}
private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)

View File

@@ -28,11 +28,11 @@ public sealed partial class DockSettingsPage : Page
{
this.InitializeComponent();
var settings = App.Current.Services.GetService<SettingsModel>()!;
var themeService = App.Current.Services.GetService<IThemeService>()!;
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
ViewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
ViewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
// Initialize UI state
InitializeSettings();
@@ -195,8 +195,7 @@ public sealed partial class DockSettingsPage : Page
// var allBands = GetAllBands();
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var settingsModel = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
var settingsModel = App.Current.Services.GetService<SettingsModel>()!;
var dockViewModel = App.Current.Services.GetService<DockViewModel>()!;
var allBands = tlcManager.GetDockBandsSnapshot();
foreach (var band in allBands)
@@ -209,7 +208,7 @@ public sealed partial class DockSettingsPage : Page
dockSettingsModel: setting,
topLevelAdapter: band,
bandViewModel: bandVm,
settingsService: settingsService
settingsModel: settingsModel
));
}
}

View File

@@ -105,11 +105,10 @@
<controls:SettingsCard x:Uid="Settings_ExtensionPage_GlobalHotkey_SettingsCard">
<cpcontrols:ShortcutControl HotkeySettings="{x:Bind Hotkey, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Name="AliasSettingsCard" x:Uid="Settings_ExtensionPage_Alias_SettingsCard">
<controls:SettingsCard x:Uid="Settings_ExtensionPage_Alias_SettingsCard">
<TextBox
x:Uid="Settings_ExtensionPage_Alias_PlaceholderText"
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.LabeledBy="{x:Bind AliasSettingsCard}"
Text="{x:Bind AliasText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_ExtensionPage_AliasActivation_SettingsCard" IsEnabled="{x:Bind AliasText, Converter={StaticResource StringEmptyToBoolConverter}, Mode=OneWay}">

View File

@@ -24,10 +24,10 @@ public sealed partial class ExtensionsPage : Page
{
this.InitializeComponent();
var settings = App.Current.Services.GetService<SettingsModel>()!;
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var themeService = App.Current.Services.GetService<IThemeService>()!;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
}
private void SettingsCard_Click(object sender, RoutedEventArgs e)

View File

@@ -23,11 +23,11 @@ public sealed partial class GeneralPage : Page
{
this.InitializeComponent();
var settings = App.Current.Services.GetService<SettingsModel>()!;
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var themeService = App.Current.Services.GetService<IThemeService>()!;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
_appInfoService = App.Current.Services.GetRequiredService<IApplicationInfoService>();
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
}
public string ApplicationVersion

View File

@@ -314,9 +314,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_ExtensionPage_Alias_SettingsCard.Description" xml:space="preserve">
<value>A short keyword used to navigate to this command.</value>
</data>
<data name="Settings_ExtensionPage_Alias_SettingsCard.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Alias</value>
</data>
<data name="Settings_ExtensionPage_AliasActivation_SettingsCard.Header" xml:space="preserve">
<value>Alias activation</value>
</data>
@@ -402,7 +399,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Dock (Preview)</value>
</data>
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Open Command Palette settings, shortcut Control plus comma</value>
<value>Open Command Palette settings</value>
</data>
<data name="ActivationSettingsHeader.Text" xml:space="preserve">
<value>Activation</value>
@@ -466,9 +463,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_ExtensionPage_Alias_PlaceholderText.PlaceholderText" xml:space="preserve">
<value>Enter alias</value>
</data>
<data name="Settings_ExtensionPage_Alias_PlaceholderText.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Alias</value>
</data>
<data name="StatusMessagesButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Show status messages</value>
</data>

View File

@@ -1,163 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
[TestClass]
public class AppStateServiceTests
{
private Mock<IPersistenceService> _mockPersistence = null!;
private Mock<IApplicationInfoService> _mockAppInfo = null!;
private string _testDirectory = null!;
[TestInitialize]
public void Setup()
{
_mockPersistence = new Mock<IPersistenceService>();
_mockAppInfo = new Mock<IApplicationInfoService>();
_testDirectory = Path.Combine(Path.GetTempPath(), $"CmdPalTest_{Guid.NewGuid():N}");
_mockAppInfo.Setup(a => a.ConfigDirectory).Returns(_testDirectory);
// Default: Load returns a new AppStateModel
_mockPersistence
.Setup(p => p.Load(
It.IsAny<string>(),
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()))
.Returns(new AppStateModel());
}
[TestMethod]
public void Constructor_LoadsState_ViaPersistenceService()
{
// Arrange
var expectedState = new AppStateModel
{
RunHistory = new List<string> { "command1", "command2" },
};
_mockPersistence
.Setup(p => p.Load(
It.IsAny<string>(),
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()))
.Returns(expectedState);
// Act
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
// Assert
Assert.IsNotNull(service.State);
Assert.AreEqual(2, service.State.RunHistory.Count);
Assert.AreEqual("command1", service.State.RunHistory[0]);
_mockPersistence.Verify(
p => p.Load(
It.IsAny<string>(),
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()),
Times.Once);
}
[TestMethod]
public void State_ReturnsLoadedModel()
{
// Arrange
var expectedState = new AppStateModel();
_mockPersistence
.Setup(p => p.Load(
It.IsAny<string>(),
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()))
.Returns(expectedState);
// Act
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
// Assert
Assert.AreSame(expectedState, service.State);
}
[TestMethod]
public void Save_DelegatesToPersistenceService()
{
// Arrange
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
service.State.RunHistory.Add("test-command");
// Act
service.Save();
// Assert
_mockPersistence.Verify(
p => p.Save(
service.State,
It.IsAny<string>(),
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<AppStateModel>>()),
Times.Once);
}
[TestMethod]
public void Save_RaisesStateChangedEvent()
{
// Arrange
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
var eventRaised = false;
service.StateChanged += (sender, state) =>
{
eventRaised = true;
};
// Act
service.Save();
// Assert
Assert.IsTrue(eventRaised);
}
[TestMethod]
public void StateChanged_PassesCorrectArguments()
{
// Arrange
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
IAppStateService? receivedSender = null;
AppStateModel? receivedState = null;
service.StateChanged += (sender, state) =>
{
receivedSender = sender;
receivedState = state;
};
// Act
service.Save();
// Assert
Assert.AreSame(service, receivedSender);
Assert.AreSame(service.State, receivedState);
}
[TestMethod]
public void Save_Always_RaisesStateChangedEvent()
{
// Arrange - AppStateService.Save() should always raise StateChanged
// (unlike SettingsService which has hotReload parameter)
var service = new AppStateService(_mockPersistence.Object, _mockAppInfo.Object);
var eventCount = 0;
service.StateChanged += (sender, state) =>
{
eventCount++;
};
// Act
service.Save();
service.Save();
// Assert
Assert.AreEqual(2, eventCount);
}
}

View File

@@ -1,136 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
[TestClass]
public partial class PersistenceServiceTests
{
private PersistenceService _service = null!;
private string _testDirectory = null!;
private string _testFilePath = null!;
// Simple test model for persistence testing
private sealed class TestModel
{
public string Name { get; set; } = string.Empty;
public int Value { get; set; }
}
[JsonSerializable(typeof(TestModel))]
private sealed partial class TestJsonContext : JsonSerializerContext
{
}
[TestInitialize]
public void Setup()
{
_service = new PersistenceService();
_testDirectory = Path.Combine(Path.GetTempPath(), $"PersistenceServiceTests_{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDirectory);
_testFilePath = Path.Combine(_testDirectory, "test.json");
}
[TestCleanup]
public void Cleanup()
{
if (Directory.Exists(_testDirectory))
{
Directory.Delete(_testDirectory, recursive: true);
}
}
[TestMethod]
public void Load_ReturnsNewInstance_WhenFileDoesNotExist()
{
// Arrange
var nonExistentPath = Path.Combine(_testDirectory, "nonexistent.json");
// Act
var result = _service.Load(nonExistentPath, TestJsonContext.Default.TestModel);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(string.Empty, result.Name);
Assert.AreEqual(0, result.Value);
}
[TestMethod]
public void Load_ReturnsDeserializedModel_WhenFileContainsValidJson()
{
// Arrange
var expectedModel = new TestModel { Name = "Test", Value = 42 };
var json = JsonSerializer.Serialize(expectedModel, TestJsonContext.Default.TestModel);
File.WriteAllText(_testFilePath, json);
// Act
var result = _service.Load(_testFilePath, TestJsonContext.Default.TestModel);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("Test", result.Name);
Assert.AreEqual(42, result.Value);
}
[TestMethod]
public void Load_ReturnsNewInstance_WhenFileContainsInvalidJson()
{
// Arrange
File.WriteAllText(_testFilePath, "{ invalid json }");
// Act
var result = _service.Load(_testFilePath, TestJsonContext.Default.TestModel);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(string.Empty, result.Name);
Assert.AreEqual(0, result.Value);
}
[TestMethod]
public void Save_CreatesFile_WhenFileDoesNotExist()
{
// Arrange
var model = new TestModel { Name = "NewFile", Value = 123 };
// Act
_service.Save(model, _testFilePath, TestJsonContext.Default.TestModel);
// Assert
Assert.IsTrue(File.Exists(_testFilePath));
var content = File.ReadAllText(_testFilePath);
Assert.IsTrue(content.Contains("NewFile"));
Assert.IsTrue(content.Contains("123"));
}
[TestMethod]
public void Save_HandlesExistingDirectory()
{
// Arrange
// Note: PersistenceService.Save() does NOT create missing directories
// It relies on Directory.CreateDirectory being called by the consumer
// (e.g., SettingsService calls Directory.CreateDirectory in SettingsJsonPath())
var nestedDir = Path.Combine(_testDirectory, "nested");
Directory.CreateDirectory(nestedDir); // Create directory beforehand
var nestedFilePath = Path.Combine(nestedDir, "test.json");
var model = new TestModel { Name = "NestedTest", Value = 321 };
// Act
_service.Save(model, nestedFilePath, TestJsonContext.Default.TestModel);
// Assert
Assert.IsTrue(File.Exists(nestedFilePath));
var result = _service.Load(nestedFilePath, TestJsonContext.Default.TestModel);
Assert.AreEqual("NestedTest", result.Name);
Assert.AreEqual(321, result.Value);
}
}

View File

@@ -1,181 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
/// <summary>
/// Tests for <see cref="SettingsService"/>.
/// NOTE: These tests currently fail in console test runners due to WinUI3 COM dependencies in SettingsModel.
/// SettingsModel constructor initializes DockSettings which uses Microsoft.UI.Colors.Transparent,
/// requiring WinUI3 runtime registration. Tests pass when run within VS Test Explorer with WinUI3 host.
/// </summary>
[TestClass]
public class SettingsServiceTests
{
private Mock<IPersistenceService> _mockPersistence = null!;
private Mock<IApplicationInfoService> _mockAppInfo = null!;
private SettingsModel _testSettings = null!;
private string _testDirectory = null!;
[TestInitialize]
public void Setup()
{
_mockPersistence = new Mock<IPersistenceService>();
_mockAppInfo = new Mock<IApplicationInfoService>();
_testDirectory = Path.Combine(Path.GetTempPath(), $"CmdPalTest_{Guid.NewGuid():N}");
_mockAppInfo.Setup(a => a.ConfigDirectory).Returns(_testDirectory);
// Create a minimal test settings instance without triggering WinUI3 dependencies
// We'll mock the Load to return this, avoiding SettingsModel constructor that uses Colors.Transparent
_testSettings = CreateMinimalSettingsModel();
// Default: Load returns our test settings
_mockPersistence
.Setup(p => p.Load(
It.IsAny<string>(),
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<SettingsModel>>()))
.Returns(_testSettings);
}
private static SettingsModel CreateMinimalSettingsModel()
{
// Bypass constructor by using deserialize from minimal JSON
// This avoids WinUI3 dependencies (Colors.Transparent)
var minimalJson = "{}";
var settings = System.Text.Json.JsonSerializer.Deserialize(
minimalJson,
JsonSerializationContext.Default.SettingsModel) ?? new SettingsModel();
return settings;
}
[TestMethod]
public void Constructor_LoadsSettings_ViaPersistenceService()
{
// Act
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
// Assert
Assert.IsNotNull(service.Settings);
_mockPersistence.Verify(
p => p.Load(
It.IsAny<string>(),
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<SettingsModel>>()),
Times.Once);
}
[TestMethod]
public void Settings_ReturnsLoadedModel()
{
// Arrange
_testSettings.ShowAppDetails = true;
// Act
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
// Assert
Assert.IsTrue(service.Settings.ShowAppDetails);
}
[TestMethod]
public void Save_DelegatesToPersistenceService()
{
// Arrange
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
service.Settings.SingleClickActivates = true;
// Act
service.Save(hotReload: false);
// Assert
_mockPersistence.Verify(
p => p.Save(
service.Settings,
It.IsAny<string>(),
It.IsAny<System.Text.Json.Serialization.Metadata.JsonTypeInfo<SettingsModel>>()),
Times.Once);
}
[TestMethod]
public void Save_WithHotReloadTrue_RaisesSettingsChangedEvent()
{
// Arrange
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
var eventRaised = false;
service.SettingsChanged += (sender, settings) =>
{
eventRaised = true;
};
// Act
service.Save(hotReload: true);
// Assert
Assert.IsTrue(eventRaised);
}
[TestMethod]
public void Save_WithHotReloadFalse_DoesNotRaiseSettingsChangedEvent()
{
// Arrange
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
var eventRaised = false;
service.SettingsChanged += (sender, settings) =>
{
eventRaised = true;
};
// Act
service.Save(hotReload: false);
// Assert
Assert.IsFalse(eventRaised);
}
[TestMethod]
public void Save_WithDefaultHotReload_RaisesSettingsChangedEvent()
{
// Arrange
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
var eventRaised = false;
service.SettingsChanged += (sender, settings) =>
{
eventRaised = true;
};
// Act
service.Save(); // Default is hotReload: true
// Assert
Assert.IsTrue(eventRaised);
}
[TestMethod]
public void SettingsChanged_PassesCorrectArguments()
{
// Arrange
var service = new SettingsService(_mockPersistence.Object, _mockAppInfo.Object);
ISettingsService? receivedSender = null;
SettingsModel? receivedSettings = null;
service.SettingsChanged += (sender, settings) =>
{
receivedSender = sender;
receivedSettings = settings;
};
// Act
service.Save(hotReload: true);
// Assert
Assert.AreSame(service, receivedSender);
Assert.AreSame(service.Settings, receivedSettings);
}
}

View File

@@ -19,6 +19,7 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem
{
private readonly AppCommand _appCommand;
private readonly AppItem _app;
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
private readonly Lazy<Task<Details>> _detailsLoadTask;

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -71,7 +70,6 @@ internal abstract partial class OnLoadContentPage : OnLoadBasePage, IContentPage
internal abstract partial class OnLoadBasePage : Page
{
private readonly Lock _loadLock = new();
private int _loadCount;
#pragma warning disable CS0067 // The event is never used
@@ -84,28 +82,22 @@ internal abstract partial class OnLoadBasePage : Page
add
{
InternalItemsChanged += value;
lock (_loadLock)
if (_loadCount == 0)
{
if (_loadCount == 0)
{
Loaded();
}
_loadCount++;
Loaded();
}
_loadCount++;
}
remove
{
InternalItemsChanged -= value;
lock (_loadLock)
_loadCount--;
_loadCount = Math.Max(0, _loadCount);
if (_loadCount == 0)
{
_loadCount--;
_loadCount = Math.Max(0, _loadCount);
if (_loadCount == 0)
{
Unloaded();
}
Unloaded();
}
}
}

View File

@@ -9,7 +9,6 @@ using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading;
using CoreWidgetProvider.Helpers;
using CoreWidgetProvider.Widgets.Enums;
using Microsoft.CmdPal.Common;
@@ -263,17 +262,17 @@ internal abstract partial class WidgetPage : OnLoadContentPage
/// </summary>
internal virtual void PushActivate()
{
Interlocked.Increment(ref _loadCount);
_loadCount++;
}
internal virtual void PopActivate()
{
Interlocked.Decrement(ref _loadCount);
_loadCount--;
}
private int _loadCount;
protected bool IsActive => Volatile.Read(ref _loadCount) > 0;
protected bool IsActive => _loadCount > 0;
protected override void Loaded()
{

View File

@@ -16,7 +16,6 @@ internal sealed partial class ApplyFancyZonesLayoutCommand : InvokableCommand
{
_layout = layout;
_targetMonitor = monitor;
Id = FancyZonesCommandIds.BuildApplyLayoutCommandId(layout, monitor);
Name = monitor is null ? "Apply to all monitors" : $"Apply to Monitor {monitor.Value.Title}";

View File

@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace PowerToysExtension.Commands;
internal sealed partial class InvokeKeyboardManagerCustomActionCommand : InvokableCommand
{
private static readonly RunnerActionClient ActionClient = new();
private readonly string _actionId;
private readonly string _fallbackError;
public InvokeKeyboardManagerCustomActionCommand(string actionId, string displayName)
{
_actionId = actionId;
_fallbackError = $"Failed to invoke {displayName}.";
Name = displayName;
}
public override CommandResult Invoke()
{
try
{
var result = ActionClient.InvokeAction(_actionId);
return result.Success
? CommandResult.Dismiss()
: CommandResult.ShowToast(string.IsNullOrWhiteSpace(result.Message) ? _fallbackError : result.Message);
}
catch (Exception ex)
{
return CommandResult.ShowToast($"{_fallbackError} {ex.Message}");
}
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
namespace PowerToysExtension.Commands;
internal sealed partial class KeyboardManagerMappingListItem : ListItem
{
public KeyboardManagerMappingListItem(KeyboardManagerMappingRecord mapping, IconInfo icon)
: base(new CommandItem(new KeyboardManagerMappingDetailsPage(mapping, icon)))
{
Title = mapping.TriggerDisplay;
Subtitle = mapping.Subtitle;
Icon = icon;
Details = BuildDetails(mapping, icon);
}
private static Details BuildDetails(KeyboardManagerMappingRecord mapping, IconInfo icon)
{
var metadata = new List<IDetailsElement>
{
DetailText("Type", mapping.Kind.ToString()),
DetailText("Target", mapping.TargetDisplay),
DetailText("Scope", mapping.IsAppSpecific ? $"App-specific ({mapping.TargetApp})" : "Global"),
};
if (!string.IsNullOrWhiteSpace(mapping.ProgramArgs))
{
metadata.Add(DetailText("Args", mapping.ProgramArgs));
}
if (!string.IsNullOrWhiteSpace(mapping.StartInDirectory))
{
metadata.Add(DetailText("Start in", mapping.StartInDirectory));
}
if (!string.IsNullOrWhiteSpace(mapping.TargetText))
{
metadata.Add(DetailText("Text", mapping.TargetText));
}
if (!string.IsNullOrWhiteSpace(mapping.UriToOpen))
{
metadata.Add(DetailText("URI", mapping.UriToOpen));
}
return new Details
{
HeroImage = icon,
Title = mapping.TriggerDisplay,
Body = mapping.Subtitle,
Metadata = metadata.ToArray(),
};
}
private static DetailsElement DetailText(string key, string value)
{
return new DetailsElement
{
Key = key,
Data = new DetailsLink { Text = value },
};
}
}

View File

@@ -3,9 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop;
namespace PowerToysExtension.Commands;
@@ -14,6 +13,8 @@ namespace PowerToysExtension.Commands;
/// </summary>
internal sealed partial class OpenNewKeyboardManagerEditorCommand : InvokableCommand
{
private static readonly RunnerActionClient ActionClient = new();
public OpenNewKeyboardManagerEditorCommand()
{
Name = "Open New Keyboard Manager Editor";
@@ -23,9 +24,10 @@ internal sealed partial class OpenNewKeyboardManagerEditorCommand : InvokableCom
{
try
{
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.OpenNewKeyboardManagerEvent());
evt.Set();
return CommandResult.Dismiss();
var result = ActionClient.InvokeAction(RunnerActionIds.KeyboardManagerOpenEditor);
return result.Success
? CommandResult.Dismiss()
: CommandResult.ShowToast(string.IsNullOrWhiteSpace(result.Message) ? "Failed to open New Keyboard Manager Editor." : result.Message);
}
catch (Exception ex)
{

View File

@@ -1,92 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace PowerToysExtension.Helpers;
internal static class FancyZonesCommandIds
{
private const string ApplyLayoutPrefix = "com.microsoft.powertoys.fancyzones.layout.apply:";
private const string AllMonitorsSuffix = ":all";
private const string MonitorMarker = ":monitor:";
public static string BuildApplyLayoutCommandId(FancyZonesLayoutDescriptor layout, FancyZonesMonitorDescriptor? monitor)
{
var escapedLayoutId = Uri.EscapeDataString(layout.Id);
return monitor is null
? $"{ApplyLayoutPrefix}{escapedLayoutId}{AllMonitorsSuffix}"
: $"{ApplyLayoutPrefix}{escapedLayoutId}{MonitorMarker}{Uri.EscapeDataString(GetMonitorToken(monitor.Value))}";
}
public static bool TryParseApplyLayoutCommandId(string id, out string layoutId, out string? monitorToken)
{
layoutId = string.Empty;
monitorToken = null;
if (string.IsNullOrWhiteSpace(id) || !id.StartsWith(ApplyLayoutPrefix, StringComparison.Ordinal))
{
return false;
}
var payload = id[ApplyLayoutPrefix.Length..];
if (payload.EndsWith(AllMonitorsSuffix, StringComparison.Ordinal))
{
var layoutPayload = payload[..^AllMonitorsSuffix.Length];
if (string.IsNullOrWhiteSpace(layoutPayload))
{
return false;
}
try
{
layoutId = Uri.UnescapeDataString(layoutPayload);
}
catch (ArgumentException)
{
layoutId = string.Empty;
return false;
}
return !string.IsNullOrWhiteSpace(layoutId);
}
var monitorMarkerIndex = payload.IndexOf(MonitorMarker, StringComparison.Ordinal);
if (monitorMarkerIndex <= 0 || monitorMarkerIndex == payload.Length - MonitorMarker.Length)
{
return false;
}
var layoutPart = payload[..monitorMarkerIndex];
var monitorPart = payload[(monitorMarkerIndex + MonitorMarker.Length)..];
if (string.IsNullOrWhiteSpace(layoutPart) || string.IsNullOrWhiteSpace(monitorPart))
{
return false;
}
try
{
layoutId = Uri.UnescapeDataString(layoutPart);
monitorToken = Uri.UnescapeDataString(monitorPart);
}
catch (ArgumentException)
{
layoutId = string.Empty;
monitorToken = null;
return false;
}
return !string.IsNullOrWhiteSpace(layoutId) && !string.IsNullOrWhiteSpace(monitorToken);
}
public static string GetMonitorToken(FancyZonesMonitorDescriptor monitor)
{
if (!string.IsNullOrWhiteSpace(monitor.Data.MonitorInstanceId))
{
return $"instance:{monitor.Data.MonitorInstanceId}";
}
return $"fallback:{monitor.Data.Monitor}|{monitor.Data.MonitorNumber}";
}
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Properties;
namespace PowerToysExtension.Helpers;
internal static class FancyZonesContextHelper
{
private static readonly CompositeFormat ApplyToMonitorFormat = CompositeFormat.Parse(Resources.FancyZones_ApplyTo_Format);
public static string FormatApplyToMonitorTitle(FancyZonesMonitorDescriptor monitor)
{
return string.Format(CultureInfo.CurrentCulture, ApplyToMonitorFormat, monitor.Title);
}
public static IContextItem[] BuildLayoutContext(FancyZonesLayoutDescriptor layout, IReadOnlyList<FancyZonesMonitorDescriptor> monitors)
{
var commands = new List<IContextItem>(monitors.Count);
foreach (var monitor in monitors)
{
commands.Add(new CommandContextItem(new ApplyFancyZonesLayoutCommand(layout, monitor))
{
Title = FormatApplyToMonitorTitle(monitor),
Subtitle = monitor.Subtitle,
});
}
return commands.ToArray();
}
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Threading;
using ManagedCommon;
using PowerToys.Interop;
namespace PowerToysExtension.Helpers;
@@ -12,6 +13,7 @@ internal static class KeyboardManagerStateService
{
private static readonly object Sync = new();
private static readonly Timer PollingTimer;
private static readonly RunnerActionClient ActionClient = new();
private static bool _lastKnownListeningState = IsListening();
internal static event Action? StatusChanged;
@@ -47,10 +49,9 @@ internal static class KeyboardManagerStateService
{
try
{
using var evt = EventWaitHandle.OpenExisting(Constants.ToggleKeyboardManagerActiveEvent());
var signaled = evt.Set();
var result = ActionClient.InvokeAction(RunnerActionIds.KeyboardManagerToggleActive);
PollStatus();
return signaled;
return result.Success;
}
catch
{

View File

@@ -38,6 +38,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.CommandPalette.Extensions" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@@ -55,7 +56,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\..\common\Common.Search\Common.Search.csproj" />
<ProjectReference Include="..\..\..\..\common\Common.UI\Common.UI.csproj" />
@@ -63,6 +63,7 @@
<ProjectReference Include="..\..\..\Awake\Awake.ModuleServices\Awake.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\colorPicker\ColorPicker.ModuleServices\ColorPicker.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj" />
<ProjectReference Include="..\..\..\keyboardmanager\KeyboardManager.ModuleServices\KeyboardManager.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\Workspaces\Workspaces.ModuleServices\Workspaces.ModuleServices.csproj" />
</ItemGroup>

View File

@@ -5,10 +5,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
@@ -16,6 +19,9 @@ namespace PowerToysExtension.Modules;
internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvider
{
private const string KeyboardManagerMappingActionPrefix = "powertoys.keyboardManager.mapping.";
private static readonly RunnerActionClient ActionClient = new();
public override IEnumerable<ListItem> BuildCommands()
{
var module = SettingsWindow.KBM;
@@ -33,9 +39,16 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
: GetResourceString("KeyboardManager_ToggleListening_Off_Subtitle", "Keyboard Manager is paused. Invoke to start listening."),
Icon = PowerToysResourcesHelper.KeyboardManagerListeningIcon(isListening),
};
yield return new ListItem(new CommandItem(new KeyboardManagerMappingsPage() { Id = "com.microsoft.powertoys.keyboardManager.mappings" }))
{
Title = "List Keyboard Manager mappings",
Subtitle = "Inspect current remaps and shortcuts from Keyboard Manager.",
Icon = icon,
};
}
if (IsUseNewEditorEnabled())
if (ModuleEnablementService.IsModuleEnabled(module) && IsUseNewEditorEnabled())
{
yield return new ListItem(new OpenNewKeyboardManagerEditorCommand())
{
@@ -45,6 +58,19 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
};
}
if (ModuleEnablementService.IsModuleEnabled(module))
{
foreach (var action in ListExecutableMappingActions())
{
yield return new ListItem(new InvokeKeyboardManagerCustomActionCommand(action.ActionId, action.DisplayName) { Id = $"com.microsoft.powertoys.keyboardManager.action.{action.ActionId}" })
{
Title = action.DisplayName,
Subtitle = string.IsNullOrWhiteSpace(action.Description) ? "Invoke a Keyboard Manager custom action." : action.Description,
Icon = icon,
};
}
}
yield return new ListItem(new OpenInSettingsCommand(module, title) { Id = "com.microsoft.powertoys.keyboardManager.openSettings" })
{
Title = title,
@@ -58,6 +84,21 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
return Resources.ResourceManager.GetString(resourceName, Resources.Culture) ?? fallback;
}
private static IEnumerable<RunnerActionDescriptor> ListExecutableMappingActions()
{
try
{
return ActionClient.ListActions()
.Where(action => action.Available && action.ActionId.StartsWith(KeyboardManagerMappingActionPrefix, StringComparison.Ordinal))
.OrderBy(action => action.DisplayName, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
}
catch
{
return Array.Empty<RunnerActionDescriptor>();
}
}
private static bool IsUseNewEditorEnabled()
{
try

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
@@ -69,7 +70,7 @@ internal sealed partial class FancyZonesLayoutsPage : DynamicListPage
var item = new FancyZonesLayoutListItem(defaultCommand, layout, fallbackIcon)
{
MoreCommands = FancyZonesContextHelper.BuildLayoutContext(layout, monitors),
MoreCommands = BuildLayoutContext(layout, monitors),
};
items.Add(item);
@@ -83,4 +84,21 @@ internal sealed partial class FancyZonesLayoutsPage : DynamicListPage
return Array.Empty<IListItem>();
}
}
private static IContextItem[] BuildLayoutContext(FancyZonesLayoutDescriptor layout, IReadOnlyList<FancyZonesMonitorDescriptor> monitors)
{
var commands = new List<IContextItem>(monitors.Count);
for (var i = 0; i < monitors.Count; i++)
{
var monitor = monitors[i];
commands.Add(new CommandContextItem(new ApplyFancyZonesLayoutCommand(layout, monitor))
{
Title = string.Format(CultureInfo.CurrentCulture, "Apply to {0}", monitor.Title),
Subtitle = monitor.Subtitle,
});
}
return commands.ToArray();
}
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace PowerToysExtension.Pages;
internal sealed partial class KeyboardManagerMappingDetailsPage : ContentPage
{
private readonly KeyboardManagerMappingRecord _mapping;
public KeyboardManagerMappingDetailsPage(KeyboardManagerMappingRecord mapping, IconInfo icon)
{
_mapping = mapping;
Icon = icon;
Name = mapping.TriggerDisplay;
Details = new Details
{
HeroImage = icon,
Title = mapping.TriggerDisplay,
Body = mapping.Subtitle,
Metadata = BuildMetadata(mapping),
};
}
public override IContent[] GetContent()
{
return
[
new MarkdownContent(
$"""
# {EscapeMarkdown(_mapping.TriggerDisplay)}
{EscapeMarkdown(_mapping.Subtitle)}
"""),
];
}
private static IDetailsElement[] BuildMetadata(KeyboardManagerMappingRecord mapping)
{
var metadata = new List<IDetailsElement>
{
DetailText("Type", mapping.Kind.ToString()),
DetailText("Scope", mapping.IsAppSpecific ? $"App-specific ({mapping.TargetApp})" : "Global"),
DetailText("Target", mapping.TargetDisplay),
};
if (!string.IsNullOrWhiteSpace(mapping.ProgramPath))
{
metadata.Add(DetailText("Program", mapping.ProgramPath));
}
if (!string.IsNullOrWhiteSpace(mapping.ProgramArgs))
{
metadata.Add(DetailText("Args", mapping.ProgramArgs));
}
if (!string.IsNullOrWhiteSpace(mapping.StartInDirectory))
{
metadata.Add(DetailText("Start in", mapping.StartInDirectory));
}
if (!string.IsNullOrWhiteSpace(mapping.UriToOpen))
{
metadata.Add(DetailText("URI", mapping.UriToOpen));
}
if (!string.IsNullOrWhiteSpace(mapping.TargetText))
{
metadata.Add(DetailText("Text", mapping.TargetText));
}
return metadata.ToArray();
}
private static DetailsElement DetailText(string key, string value)
{
return new DetailsElement
{
Key = key,
Data = new DetailsLink { Text = value },
};
}
private static string EscapeMarkdown(string value)
{
return value.Replace("\\", "\\\\").Replace("*", "\\*").Replace("_", "\\_");
}
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
namespace PowerToysExtension.Pages;
internal sealed partial class KeyboardManagerMappingsPage : DynamicListPage
{
private readonly CommandItem _emptyMessage;
private readonly IconInfo _icon;
public KeyboardManagerMappingsPage()
{
_icon = PowerToysResourcesHelper.IconFromSettingsIcon("KeyboardManager.png");
Icon = _icon;
Name = Title = "Keyboard Manager mappings";
Id = "com.microsoft.cmdpal.powertoys.keyboardManager.mappings";
ShowDetails = true;
_emptyMessage = new CommandItem
{
Title = "No Keyboard Manager mappings found",
Subtitle = "Create mappings in Keyboard Manager to inspect them here.",
Icon = _icon,
};
EmptyContent = _emptyMessage;
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
RaiseItemsChanged(0);
}
public override IListItem[] GetItems()
{
var result = KeyboardManagerMappingService.Instance.GetMappingsAsync().GetAwaiter().GetResult();
if (!result.Success || result.Value is null)
{
_emptyMessage.Subtitle = result.Error ?? "Failed to read Keyboard Manager mappings.";
return Array.Empty<IListItem>();
}
IEnumerable<KeyboardManagerMappingRecord> mappings = result.Value;
if (!string.IsNullOrWhiteSpace(SearchText))
{
mappings = mappings.Where(mapping =>
mapping.TriggerDisplay.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.TargetDisplay.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.TargetApp.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase));
}
return mappings
.OrderBy(mapping => mapping.TriggerDisplay, StringComparer.CurrentCultureIgnoreCase)
.Select(mapping => (IListItem)new KeyboardManagerMappingListItem(mapping, _icon))
.ToArray();
}
}

View File

@@ -2,14 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using PowerToysExtension.Properties;
namespace PowerToysExtension;
@@ -68,54 +64,11 @@ public partial class PowerToysExtensionCommandsProvider : CommandProvider
}
}
return TryGetFancyZonesCommandItem(id);
return null;
}
private void RaiseModuleItemsChanged()
{
RaiseItemsChanged();
}
private static ICommandItem? TryGetFancyZonesCommandItem(string id)
{
if (!FancyZonesCommandIds.TryParseApplyLayoutCommandId(id, out var layoutId, out var monitorToken))
{
return null;
}
var layout = FancyZonesDataService.GetLayouts()
.FirstOrDefault(candidate => string.Equals(candidate.Id, layoutId, StringComparison.Ordinal));
if (layout is null)
{
return null;
}
var fallbackIcon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png");
if (string.IsNullOrWhiteSpace(monitorToken))
{
FancyZonesDataService.TryGetMonitors(out var monitors, out _);
return new FancyZonesLayoutListItem(new ApplyFancyZonesLayoutCommand(layout, monitor: null), layout, fallbackIcon)
{
MoreCommands = FancyZonesContextHelper.BuildLayoutContext(layout, monitors),
};
}
if (!FancyZonesDataService.TryGetMonitors(out var availableMonitors, out _))
{
return null;
}
var monitor = availableMonitors
.FirstOrDefault(candidate => string.Equals(FancyZonesCommandIds.GetMonitorToken(candidate), monitorToken, StringComparison.Ordinal));
if (monitor.Equals(default(FancyZonesMonitorDescriptor)))
{
return null;
}
return new FancyZonesLayoutListItem(new ApplyFancyZonesLayoutCommand(layout, monitor), layout, fallbackIcon)
{
Subtitle = FancyZonesContextHelper.FormatApplyToMonitorTitle(monitor),
};
}
}

View File

@@ -1,36 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension.Pages;
public sealed partial class SampleGoToLandingPage : ListPage
{
public const string PageId = "com.microsoft.SamplePages.GoToLandingPage";
public SampleGoToLandingPage()
{
Icon = new IconInfo("\uEA37");
Name = "GoToPage Sample: Landing Page";
Id = PageId;
}
public override IListItem[] GetItems()
{
return [
new ListItem(new NoOpCommand())
{
Title = "You have arrived at the landing page!",
Subtitle = "This page was navigated to via CommandResult.GoToPage()",
},
new ListItem(new NoOpCommand())
{
Title = "This is a sample landing page",
Subtitle = "Extensions can use GoToPage to direct users here after completing an action",
},
];
}
}

View File

@@ -1,78 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension.Pages;
/// <summary>
/// A sample page that demonstrates the CommandResult.GoToPage() feature.
/// It shows three different NavigationMode options:
/// Push - adds the landing page on top of the current stack
/// GoBack - goes back one level, then navigates to the landing page
/// GoHome - clears the entire stack, then navigates to the landing page
/// </summary>
public sealed partial class SampleGoToPage : ListPage
{
public SampleGoToPage()
{
Icon = new IconInfo("\uEA37");
Name = "GoToPage Sample";
}
public override IListItem[] GetItems()
{
var pushArgs = new GoToPageArgs
{
PageId = SampleGoToLandingPage.PageId,
NavigationMode = NavigationMode.Push,
};
var goBackArgs = new GoToPageArgs
{
PageId = SampleGoToLandingPage.PageId,
NavigationMode = NavigationMode.GoBack,
};
var goHomeArgs = new GoToPageArgs
{
PageId = SampleGoToLandingPage.PageId,
NavigationMode = NavigationMode.GoHome,
};
return [
new ListItem(
new AnonymousCommand(() => { })
{
Result = CommandResult.GoToPage(pushArgs),
})
{
Title = "Push: Navigate to landing page (keep back stack)",
Subtitle = "Adds the landing page on top of the current navigation stack",
Icon = new IconInfo("\uEA37"),
},
new ListItem(
new AnonymousCommand(() => { })
{
Result = CommandResult.GoToPage(goBackArgs),
})
{
Title = "GoBack: Go back one level, then navigate to landing page",
Subtitle = "Removes one page from the back stack before navigating",
Icon = new IconInfo("\uE760"),
},
new ListItem(
new AnonymousCommand(() => { })
{
Result = CommandResult.GoToPage(goHomeArgs),
})
{
Title = "GoHome: Clear stack, then navigate to landing page",
Subtitle = "Clears the entire navigation stack before navigating",
Icon = new IconInfo("\uE80F"),
},
];
}
}

View File

@@ -22,11 +22,6 @@ public partial class SamplePagesCommandsProvider : CommandProvider
Title = "Sample Pages",
Subtitle = "View example commands",
},
new CommandItem(new Pages.SampleGoToLandingPage())
{
Title = "GoToPage Sample: Landing Page",
Subtitle = "Target landing page for the GoToPage sample",
},
];
public override ICommandItem[] TopLevelCommands()

View File

@@ -124,12 +124,7 @@ public partial class SamplesListPage : ListPage
{
Title = "Issue-specific samples",
Subtitle = "Samples designed to reproduce specific issues",
},
new ListItem(new SampleGoToPage())
{
Title = "GoToPage Sample",
Subtitle = "Demonstrates CommandResult.GoToPage() with Push, GoBack, and GoHome modes",
},
}
];
public SamplesListPage()

View File

@@ -1,7 +1,8 @@
#pragma once
#include <compare>
#include <common/utils/gpo.h>
#pragma once
#include <compare>
#include <cwchar>
#include <common/utils/gpo.h>
/*
DLL Interface for PowerToys. The powertoy_create() (see below) must return
@@ -97,10 +98,53 @@ public:
virtual bool get_config(wchar_t* buffer, int* buffer_size) = 0;
/* Sets the configuration values. */
virtual void set_config(const wchar_t* config) = 0;
/* Call custom action from settings screen. */
virtual void call_custom_action(const wchar_t* /*action*/){};
/* Enables the PowerToy. */
virtual void enable() = 0;
/* Call custom action from settings screen. */
virtual void call_custom_action(const wchar_t* /*action*/){};
/* Returns the actions exposed by the module as a JSON array. */
virtual bool get_actions(wchar_t* buffer, int* buffer_size)
{
constexpr const wchar_t empty_actions[] = L"[]";
if (!buffer_size)
{
return false;
}
const int required_size = static_cast<int>(sizeof(empty_actions) / sizeof(empty_actions[0]));
if (!buffer || *buffer_size < required_size)
{
*buffer_size = required_size;
return false;
}
wcscpy_s(buffer, *buffer_size, empty_actions);
return true;
}
/* Invokes an action exposed by the module and returns a JSON object result. */
virtual bool invoke_action(const wchar_t* /*action_id*/, const wchar_t* /*serialized_args*/, wchar_t* buffer, int* buffer_size)
{
constexpr const wchar_t unsupported_result[] =
L"{\"success\":false,\"error_code\":\"not_supported\",\"message\":\"This module does not expose actions.\"}";
if (!buffer_size)
{
return false;
}
const int required_size = static_cast<int>(sizeof(unsupported_result) / sizeof(unsupported_result[0]));
if (!buffer || *buffer_size < required_size)
{
*buffer_size = required_size;
return false;
}
wcscpy_s(buffer, *buffer_size, unsupported_result);
return true;
}
/* Enables the PowerToy. */
virtual void enable() = 0;
/* Disables the PowerToy, should free as much memory as possible. */
virtual void disable() = 0;
/* Should return if the PowerToys is enabled or disabled. */

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<Import Project="$(RepoRoot)src\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\PowerToys.ModuleContracts\PowerToys.ModuleContracts.csproj" />
<ProjectReference Include="..\KeyboardManagerEditorLibraryWrapper\KeyboardManagerEditorLibraryWrapper.vcxproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using System.Text;
namespace KeyboardManager.ModuleServices;
internal static class KeyboardManagerInterop
{
private const string DllName = "Powertoys.KeyboardManagerEditorLibraryWrapper.dll";
private const CallingConvention Convention = CallingConvention.Cdecl;
[DllImport(DllName, CallingConvention = Convention)]
internal static extern IntPtr CreateMappingConfiguration();
[DllImport(DllName, CallingConvention = Convention)]
internal static extern void DestroyMappingConfiguration(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool LoadMappingSettings(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetSingleKeyRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyRemap(IntPtr config, int index, ref NativeSingleKeyMapping mapping);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetSingleKeyToTextRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyToTextRemap(IntPtr config, int index, ref NativeKeyboardTextMapping mapping);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetShortcutRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetShortcutRemap(IntPtr config, int index, ref NativeShortcutMapping mapping);
[DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
internal static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern void FreeString(IntPtr str);
internal static string GetStringAndFree(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
return string.Empty;
}
var result = Marshal.PtrToStringUni(handle) ?? string.Empty;
FreeString(handle);
return result;
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeSingleKeyMapping
{
public int OriginalKey;
public IntPtr TargetKey;
[MarshalAs(UnmanagedType.Bool)]
public bool IsShortcut;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeKeyboardTextMapping
{
public int OriginalKey;
public IntPtr TargetText;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeShortcutMapping
{
public IntPtr OriginalKeys;
public IntPtr TargetKeys;
public IntPtr TargetApp;
public int OperationType;
public IntPtr TargetText;
public IntPtr ProgramPath;
public IntPtr ProgramArgs;
public IntPtr StartInDirectory;
public int Elevation;
public int IfRunningAction;
public int Visibility;
public IntPtr UriToOpen;
}

View File

@@ -0,0 +1,56 @@
// 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 KeyboardManager.ModuleServices;
public enum KeyboardManagerMappingKind
{
SingleKeyToKey,
SingleKeyToShortcut,
SingleKeyToText,
ShortcutToKey,
ShortcutToShortcut,
ShortcutToText,
ShortcutToProgram,
ShortcutToUri,
}
public sealed class KeyboardManagerMappingRecord
{
public string Id { get; init; } = string.Empty;
public KeyboardManagerMappingKind Kind { get; init; }
public string TriggerDisplay { get; init; } = string.Empty;
public string TargetDisplay { get; init; } = string.Empty;
public string Subtitle { get; init; } = string.Empty;
public bool IsAppSpecific { get; init; }
public string TargetApp { get; init; } = string.Empty;
public string OriginalKeys { get; init; } = string.Empty;
public string TargetKeys { get; init; } = string.Empty;
public string TargetText { get; init; } = string.Empty;
public string ProgramPath { get; init; } = string.Empty;
public string ProgramArgs { get; init; } = string.Empty;
public string StartInDirectory { get; init; } = string.Empty;
public int Elevation { get; init; }
public int IfRunningAction { get; init; }
public int Visibility { get; init; }
public string UriToOpen { get; init; } = string.Empty;
public bool IsExecutable => Kind is KeyboardManagerMappingKind.ShortcutToProgram or KeyboardManagerMappingKind.ShortcutToUri;
}

View File

@@ -0,0 +1,236 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using ManagedCommon;
namespace KeyboardManager.ModuleServices;
public sealed class KeyboardManagerMappingService
{
public static KeyboardManagerMappingService Instance { get; } = new();
public Task<PowerToys.ModuleContracts.OperationResult<IReadOnlyList<KeyboardManagerMappingRecord>>> GetMappingsAsync(CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
using var session = new MappingConfigurationSession();
if (!KeyboardManagerInterop.LoadMappingSettings(session.Handle))
{
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>("Failed to load Keyboard Manager mappings."));
}
var mappings = new List<KeyboardManagerMappingRecord>();
LoadSingleKeyMappings(session.Handle, mappings);
LoadSingleKeyTextMappings(session.Handle, mappings);
LoadShortcutMappings(session.Handle, mappings);
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Ok<IReadOnlyList<KeyboardManagerMappingRecord>>(mappings));
}
catch (OperationCanceledException)
{
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>("Keyboard Manager mapping query was cancelled."));
}
catch (Exception ex)
{
Logger.LogError($"Failed to enumerate Keyboard Manager mappings: {ex.Message}");
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>($"Failed to enumerate Keyboard Manager mappings: {ex.Message}"));
}
}
private static void LoadSingleKeyMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetSingleKeyRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeSingleKeyMapping);
if (!KeyboardManagerInterop.GetSingleKeyRemap(handle, i, ref native))
{
continue;
}
var originalDisplay = GetKeyDisplayName(native.OriginalKey);
var targetRaw = KeyboardManagerInterop.GetStringAndFree(native.TargetKey);
var targetDisplay = native.IsShortcut ? FormatShortcut(targetRaw) : FormatSingleKey(targetRaw);
var kind = native.IsShortcut ? KeyboardManagerMappingKind.SingleKeyToShortcut : KeyboardManagerMappingKind.SingleKeyToKey;
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("single-key", native.OriginalKey.ToString(CultureInfo.InvariantCulture), targetRaw, native.IsShortcut.ToString()),
Kind = kind,
TriggerDisplay = originalDisplay,
TargetDisplay = targetDisplay,
Subtitle = $"Maps to {targetDisplay}",
OriginalKeys = native.OriginalKey.ToString(CultureInfo.InvariantCulture),
TargetKeys = targetRaw,
});
}
}
private static void LoadSingleKeyTextMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetSingleKeyToTextRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeKeyboardTextMapping);
if (!KeyboardManagerInterop.GetSingleKeyToTextRemap(handle, i, ref native))
{
continue;
}
var originalDisplay = GetKeyDisplayName(native.OriginalKey);
var targetText = KeyboardManagerInterop.GetStringAndFree(native.TargetText);
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("single-key-text", native.OriginalKey.ToString(CultureInfo.InvariantCulture), targetText),
Kind = KeyboardManagerMappingKind.SingleKeyToText,
TriggerDisplay = originalDisplay,
TargetDisplay = targetText,
Subtitle = $"Types {targetText}",
OriginalKeys = native.OriginalKey.ToString(CultureInfo.InvariantCulture),
TargetText = targetText,
});
}
}
private static void LoadShortcutMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetShortcutRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeShortcutMapping);
if (!KeyboardManagerInterop.GetShortcutRemap(handle, i, ref native))
{
continue;
}
var originalKeys = KeyboardManagerInterop.GetStringAndFree(native.OriginalKeys);
var targetKeys = KeyboardManagerInterop.GetStringAndFree(native.TargetKeys);
var targetApp = KeyboardManagerInterop.GetStringAndFree(native.TargetApp);
var targetText = KeyboardManagerInterop.GetStringAndFree(native.TargetText);
var programPath = KeyboardManagerInterop.GetStringAndFree(native.ProgramPath);
var programArgs = KeyboardManagerInterop.GetStringAndFree(native.ProgramArgs);
var startInDirectory = KeyboardManagerInterop.GetStringAndFree(native.StartInDirectory);
var uriToOpen = KeyboardManagerInterop.GetStringAndFree(native.UriToOpen);
var triggerDisplay = FormatShortcut(originalKeys);
var (kind, targetDisplay, subtitle) = DescribeShortcutTarget(native.OperationType, targetKeys, targetText, programPath, uriToOpen);
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("shortcut", originalKeys, targetKeys, targetText, programPath, programArgs, startInDirectory, uriToOpen, targetApp, native.OperationType.ToString(CultureInfo.InvariantCulture)),
Kind = kind,
TriggerDisplay = triggerDisplay,
TargetDisplay = targetDisplay,
Subtitle = string.IsNullOrWhiteSpace(targetApp) ? subtitle : $"{subtitle} in {targetApp}",
IsAppSpecific = !string.IsNullOrWhiteSpace(targetApp),
TargetApp = targetApp,
OriginalKeys = originalKeys,
TargetKeys = targetKeys,
TargetText = targetText,
ProgramPath = programPath,
ProgramArgs = programArgs,
StartInDirectory = startInDirectory,
Elevation = native.Elevation,
IfRunningAction = native.IfRunningAction,
Visibility = native.Visibility,
UriToOpen = uriToOpen,
});
}
}
private static (KeyboardManagerMappingKind Kind, string TargetDisplay, string Subtitle) DescribeShortcutTarget(int operationType, string targetKeys, string targetText, string programPath, string uriToOpen)
{
return operationType switch
{
1 => (KeyboardManagerMappingKind.ShortcutToProgram, programPath, $"Opens {programPath}"),
2 => (KeyboardManagerMappingKind.ShortcutToUri, uriToOpen, $"Opens {uriToOpen}"),
3 => (KeyboardManagerMappingKind.ShortcutToText, targetText, $"Types {targetText}"),
_ => DescribeShortcutRemap(targetKeys),
};
}
private static (KeyboardManagerMappingKind Kind, string TargetDisplay, string Subtitle) DescribeShortcutRemap(string targetKeys)
{
var keyCodes = ParseKeyCodes(targetKeys);
if (keyCodes.Count <= 1)
{
var targetDisplay = FormatSingleKey(targetKeys);
return (KeyboardManagerMappingKind.ShortcutToKey, targetDisplay, $"Maps to {targetDisplay}");
}
var shortcutDisplay = FormatShortcut(targetKeys);
return (KeyboardManagerMappingKind.ShortcutToShortcut, shortcutDisplay, $"Maps to {shortcutDisplay}");
}
private static string FormatSingleKey(string value)
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var keyCode))
{
return GetKeyDisplayName(keyCode);
}
return value;
}
private static string FormatShortcut(string value)
{
var parts = ParseKeyCodes(value)
.Select(GetKeyDisplayName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToArray();
return parts.Length == 0 ? value : string.Join(" + ", parts);
}
private static IReadOnlyList<int> ParseKeyCodes(string value)
{
return value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(part => int.TryParse(part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var keyCode) ? keyCode : (int?)null)
.Where(keyCode => keyCode.HasValue)
.Select(keyCode => keyCode!.Value)
.ToArray();
}
private static string GetKeyDisplayName(int keyCode)
{
var buffer = new StringBuilder(64);
KeyboardManagerInterop.GetKeyDisplayName(keyCode, buffer, buffer.Capacity);
return buffer.ToString();
}
private static string CreateId(params string[] values)
{
var payload = string.Join("|", values);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(payload));
return Convert.ToHexString(hash[..8]);
}
private sealed partial class MappingConfigurationSession : IDisposable
{
public MappingConfigurationSession()
{
Handle = KeyboardManagerInterop.CreateMappingConfiguration();
if (Handle == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create Keyboard Manager mapping configuration.");
}
}
public IntPtr Handle { get; private set; }
public void Dispose()
{
if (Handle != IntPtr.Zero)
{
KeyboardManagerInterop.DestroyMappingConfiguration(Handle);
Handle = IntPtr.Zero;
}
}
}
}

View File

@@ -29,7 +29,7 @@
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<AdditionalIncludeDirectories>./;$(SolutionDir)src\modules\;$(SolutionDir)src\common\Display;$(SolutionDir)src\common\inc;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>./;$(RepoRoot)src\modules\;$(RepoRoot)src\common\Display;$(RepoRoot)src\common\inc;$(RepoRoot)src\common\Telemetry;$(RepoRoot)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
@@ -117,6 +117,6 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
</Target>
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
</Target>
</Project>
</Project>

View File

@@ -335,6 +335,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
@@ -351,6 +355,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->startInDirectory = AllocateAndCopyString(targetShortcut.runProgramStartInDir);
mapping->elevation = static_cast<int>(targetShortcut.elevationLevel);
mapping->ifRunningAction = static_cast<int>(targetShortcut.alreadyRunningAction);
mapping->visibility = static_cast<int>(targetShortcut.startWindowType);
mapping->uriToOpen = AllocateAndCopyString(L"");
break;
@@ -359,6 +367,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
break;
@@ -367,6 +379,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
break;
}
@@ -375,10 +391,14 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 0;
mapping->operationType = 3;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
@@ -438,6 +458,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
@@ -453,6 +477,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->startInDirectory = AllocateAndCopyString(targetShortcut.runProgramStartInDir);
mapping->elevation = static_cast<int>(targetShortcut.elevationLevel);
mapping->ifRunningAction = static_cast<int>(targetShortcut.alreadyRunningAction);
mapping->visibility = static_cast<int>(targetShortcut.startWindowType);
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcut.operationType == Shortcut::OperationType::OpenURI)
@@ -461,6 +489,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
}
else
@@ -469,6 +501,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
}
@@ -476,10 +512,14 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 0;
mapping->operationType = 3;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
@@ -749,4 +789,4 @@ int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount)
}
return count;
}
}

View File

@@ -32,6 +32,10 @@ struct ShortcutMapping
wchar_t* targetText;
wchar_t* programPath;
wchar_t* programArgs;
wchar_t* startInDirectory;
int elevation;
int ifRunningAction;
int visibility;
wchar_t* uriToOpen;
};

View File

@@ -428,20 +428,20 @@ namespace Helpers
{
while (true)
{
std::wstring text;
{
std::unique_lock<std::mutex> lock(s_queueMutex);
s_queueCV.wait(lock, [] { return !s_clipboardQueue.empty() || s_shutdown.load(); });
if (s_shutdown.load())
{
break;
}
text = std::move(s_clipboardQueue.front());
s_clipboardQueue.pop();
}
try
{
std::wstring text;
{
std::unique_lock<std::mutex> lock(s_queueMutex);
s_queueCV.wait(lock, [] { return !s_clipboardQueue.empty() || s_shutdown.load(); });
if (s_shutdown.load())
{
break;
}
text = std::move(s_clipboardQueue.front());
s_clipboardQueue.pop();
}
// Snapshot current clipboard state
bool hadOriginalText = false;
std::wstring originalClipboardText;
@@ -490,15 +490,22 @@ namespace Helpers
}
}
}
catch (const std::exception& ex)
{
OutputDebugStringA("KBM ClipboardWorker exception: ");
OutputDebugStringA(ex.what());
OutputDebugStringA("\n");
}
catch (...)
{
OutputDebugStringA("KBM ClipboardWorker exception caught, continuing\n");
OutputDebugStringA("KBM ClipboardWorker unknown exception\n");
}
}
}
// Inner implementation that may throw C++ exceptions.
static bool SendTextViaClipboardImpl(const std::wstring& text)
// Function to send text via clipboard paste (Ctrl+V).
// Saves the previous clipboard content and restores it asynchronously.
bool SendTextViaClipboard(const std::wstring& text)
{
// Lazily start the worker on first use.
std::call_once(s_workerInitFlag, [] {
@@ -517,7 +524,7 @@ namespace Helpers
});
// Enqueue the text and return immediately so we never block the
// low-level keyboard hook (WH_KEYBOARD_LL).
// low-level keyboard hook (WH_KEYBOARD_LL).
{
std::lock_guard<std::mutex> lock(s_queueMutex);
if (!s_clipboardQueue.empty())
@@ -531,24 +538,6 @@ namespace Helpers
return true;
}
// Function to send text via clipboard paste (Ctrl+V).
// Saves the previous clipboard content and restores it asynchronously.
// This function MUST NOT throw — it's called from noexcept hook handlers.
// We use __try/__except (SEH) because C++ try/catch may be optimized away
// in Release builds when the caller is noexcept.
bool SendTextViaClipboard(const std::wstring& text) noexcept
{
__try
{
return SendTextViaClipboardImpl(text);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
OutputDebugStringA("KBM SendTextViaClipboard SEH exception caught\n");
return false;
}
}
// Function to filter the key codes for artificial key codes
int32_t FilterArtificialKeys(const int32_t& key)
{

View File

@@ -42,7 +42,7 @@ namespace Helpers
void SetTextKeyEvents(std::vector<INPUT>& keyEventArray, const std::wstring& remapping);
// Function to send text via clipboard paste (Ctrl+V). Saves and restores previous clipboard content.
bool SendTextViaClipboard(const std::wstring& text) noexcept;
bool SendTextViaClipboard(const std::wstring& text);
// Function to return window handle for a full screen UWP app
HWND GetFullscreenUWPWindowHandle();

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