mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-05 09:59:28 +02:00
Compare commits
32 Commits
gleb/advan
...
yuleng/kbm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c27ce19ce2 | ||
|
|
46e3215c10 | ||
|
|
f0a828ee22 | ||
|
|
b2bd24db0d | ||
|
|
28d6fe1615 | ||
|
|
fbc1a0c3da | ||
|
|
20df1fd96e | ||
|
|
71d91a9616 | ||
|
|
bd97ba31e8 | ||
|
|
245a6db963 | ||
|
|
314f9fe751 | ||
|
|
832db1bfea | ||
|
|
412028c861 | ||
|
|
dda2a89aa6 | ||
|
|
164ac6074a | ||
|
|
09cb927356 | ||
|
|
78c0e3e131 | ||
|
|
ebc44a0e9d | ||
|
|
d88dca2c1e | ||
|
|
f893fc7a77 | ||
|
|
7c3c5514ee | ||
|
|
3a012d4cf1 | ||
|
|
9d58b1bdd1 | ||
|
|
6fd36f9579 | ||
|
|
a8b79158f1 | ||
|
|
ff578d15a3 | ||
|
|
bccceba97b | ||
|
|
e03d048e8a | ||
|
|
806ff3c07a | ||
|
|
c1ecdda60c | ||
|
|
43e530d2e1 | ||
|
|
016e0732b0 |
5
.github/actions/spell-check/allow/code.txt
vendored
5
.github/actions/spell-check/allow/code.txt
vendored
@@ -309,11 +309,6 @@ pwa
|
||||
AOT
|
||||
Aot
|
||||
ify
|
||||
LAF
|
||||
Laf
|
||||
languagemodel
|
||||
philm
|
||||
phisilica
|
||||
TFM
|
||||
|
||||
# YML
|
||||
|
||||
3
.github/actions/spell-check/expect.txt
vendored
3
.github/actions/spell-check/expect.txt
vendored
@@ -1396,6 +1396,7 @@ POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
POWERTOYNAME
|
||||
powertoyscli
|
||||
powertoyssetup
|
||||
powertoysusersetup
|
||||
Powrprof
|
||||
@@ -1556,6 +1557,7 @@ RESIZETOFIT
|
||||
resmimetype
|
||||
RESOURCEID
|
||||
RESTORETOMAXIMIZED
|
||||
retargets
|
||||
RETURNONLYFSDIRS
|
||||
Revalidates
|
||||
RGBQUAD
|
||||
@@ -1602,6 +1604,7 @@ sancov
|
||||
SAVEFAILED
|
||||
schedtasks
|
||||
SCID
|
||||
SCL
|
||||
Scode
|
||||
SCREENFONTS
|
||||
screenruler
|
||||
|
||||
3
.github/actions/spell-check/patterns.txt
vendored
3
.github/actions/spell-check/patterns.txt
vendored
@@ -313,9 +313,6 @@ ms-windows-store://\S+
|
||||
# ANSI color codes
|
||||
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+)*m
|
||||
|
||||
# Phi Silica internal token/ID literals
|
||||
<PhiSilicaLafToken\b[^>]*>[^<]+</PhiSilicaLafToken>
|
||||
\bdjwsxzxb4ksa8\b
|
||||
# Special licenses text from RNNoise (BSD-style disclaimer: ``AS IS'')
|
||||
``AS IS''
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -378,6 +378,3 @@ installer/*/*.wxs.bk
|
||||
vcpkg_installed/
|
||||
|
||||
deps/vcpkg/
|
||||
|
||||
# Superpowers-generated docs (specs, design, plans) — local-only, not committed
|
||||
docs/superpowers/
|
||||
|
||||
@@ -102,7 +102,7 @@ extends:
|
||||
useManagedIdentity: $(SigningUseManagedIdentity)
|
||||
clientId: $(SigningOriginalClientId)
|
||||
# Have msbuild use the release nuget config profile
|
||||
additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:EnableCmdPalAOT=true /p:PhiSilicaLafToken=$(PhiSilicaLafToken) /p:PhiSilicaLafAttestation="$(PhiSilicaLafAttestation)"
|
||||
additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:EnableCmdPalAOT=true
|
||||
beforeBuildSteps:
|
||||
# Install the Terrapin retrieval tool, which replaces vcpkg's download handler
|
||||
# to redirect it to a safe Microsoft-controlled location
|
||||
|
||||
@@ -266,17 +266,6 @@ jobs:
|
||||
VCWhereExtraVersionTarget: '-prerelease'
|
||||
|
||||
- ${{ if eq(parameters.official, true) }}:
|
||||
# M.W.T.V Setup.ps1 sets the pipeline-level XES_APPXMANIFESTVERSION env var
|
||||
# from the supplied -ProjectDirectory's custom.props. Whichever invocation
|
||||
# runs last wins. cmdpal MUST run last because $(CmdPalVersion) (used by the
|
||||
# VNext installer and CmdPal's AppxPackageTestDir) falls back to that env
|
||||
# var; if AP wins, CmdPal's folder name and MSIX filename disagree on
|
||||
# VersionMinor and the installer fails with WIX0103. Each project's own
|
||||
# custom.props is still applied at MSBuild time, so AP versioning is
|
||||
# unaffected by the order.
|
||||
- template: .\steps-setup-versioning.yml
|
||||
parameters:
|
||||
directory: $(build.sourcesdirectory)\src\modules\AdvancedPaste
|
||||
- template: .\steps-setup-versioning.yml
|
||||
parameters:
|
||||
directory: $(build.sourcesdirectory)\src\modules\cmdpal
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<RepoRoot>$(MSBuildThisFileDirectory)</RepoRoot>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(RepoRoot)src\Version.props" />
|
||||
<Import Project="$(RepoRoot)src\PhiSilicaLaf.props" />
|
||||
<PropertyGroup>
|
||||
<Copyright>Copyright (C) Microsoft Corporation. All rights reserved.</Copyright>
|
||||
<AssemblyCopyright>Copyright (C) Microsoft Corporation. All rights reserved.</AssemblyCopyright>
|
||||
|
||||
@@ -78,10 +78,10 @@
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.1.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="2.2.3" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.0.20" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="2.0.185" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="2.0.1" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
|
||||
@@ -500,6 +500,10 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditorUI.UnitTests/KeyboardManagerEditorUI.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngine/KeyboardManagerEngine.vcxproj" Id="ba661f5b-1d5a-4ffc-9bf1-fc39df280bdd" />
|
||||
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManagerEngineLibrary.vcxproj" Id="e496b7fc-1e99-4bab-849b-0e8367040b02" />
|
||||
</Folder>
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# Advanced Paste – Phi Silica local testing
|
||||
|
||||
How to build, register, and test **Phi Silica** in **Advanced Paste (AP)** on a dev machine,
|
||||
plus the few things that actually break it.
|
||||
|
||||
## How it fits together
|
||||
|
||||
AP ships as an **unpackaged, self-contained WinUI 3 exe** (`PowerToys.AdvancedPaste.exe`).
|
||||
The Windows AI `LanguageModel` (Phi Silica) API is a **Limited Access Feature (LAF)**. For it
|
||||
to work, all of these must line up:
|
||||
|
||||
1. **Package identity** — AP runs with identity granted by the sparse MSIX
|
||||
`Microsoft.PowerToys.SparseApp`.
|
||||
2. **Matching LAF creds** — the token/attestation baked into the exe match the registered
|
||||
sparse package's publisher.
|
||||
3. **AI metadata deployed** — the `Microsoft.Windows.AI*.winmd` files ship next to the exe;
|
||||
the AI runtime resolves them **at runtime**.
|
||||
4. **Model ready** — supported hardware and the on-device model downloaded
|
||||
(`GetReadyState() == Ready`).
|
||||
|
||||
Two identities — the baked token must match the registered package's publisher:
|
||||
|
||||
| Build | Publisher Id | LAF creds |
|
||||
|-------|--------------|-----------|
|
||||
| **Dev** | `djwsxzxb4ksa8` | dev default in [`src/PhiSilicaLaf.props`](../../../src/PhiSilicaLaf.props) |
|
||||
| **Prod** | `8wekyb3d8bbwe` | secret, injected only by `.pipelines/v2/release.yml` |
|
||||
|
||||
Non-secret pairing check: the exe's baked **Attestation** must equal the registered package's
|
||||
**PublisherId**.
|
||||
|
||||
## Build + register (dev loop)
|
||||
|
||||
```powershell
|
||||
$repo = "X:\GitHub\PowerToys"; $Plat = "ARM64"; $Cfg = "Debug" # or x64 / Release
|
||||
|
||||
# Build AP only (C#; reuses existing C++ outputs):
|
||||
dotnet restore "$repo\src\modules\AdvancedPaste\AdvancedPaste\AdvancedPaste.csproj" /p:Platform=$Plat
|
||||
& "$repo\tools\build\build.cmd" -Path "$repo\src\modules\AdvancedPaste\AdvancedPaste" `
|
||||
-Platform $Plat -Configuration $Cfg /p:BuildProjectReferences=false
|
||||
|
||||
# Register the dev sparse package (creates + trusts a dev cert, grants identity):
|
||||
pwsh -ExecutionPolicy Bypass -File "$repo\src\PackageIdentity\BuildSparsePackage.ps1" `
|
||||
-Platform $Plat -Configuration $Cfg -DevRegister
|
||||
# Expect: PublisherId djwsxzxb4ksa8, IsDevelopmentMode True
|
||||
```
|
||||
|
||||
## Check the API
|
||||
|
||||
`PowerToys.AdvancedPaste.exe` is a **GUI-subsystem** app — run directly in a console it prints
|
||||
nothing and returns no exit code. **Redirect** stdout/stderr and wait:
|
||||
|
||||
```powershell
|
||||
$exe = "$repo\$Plat\$Cfg\WinUI3Apps\PowerToys.AdvancedPaste.exe"
|
||||
$o = "$env:TEMP\ap.out"; $e = "$env:TEMP\ap.err"
|
||||
$p = Start-Process $exe '--check-phi-silica' -Wait -PassThru -WindowStyle Hidden `
|
||||
-RedirectStandardOutput $o -RedirectStandardError $e
|
||||
"exit=$($p.ExitCode) stdout=$((Get-Content $o -Raw).Trim())"
|
||||
Get-Content $e -Raw # stderr: [phi-silica] LAF unlock status: <…>; ReadyState: <…>
|
||||
```
|
||||
|
||||
| `--check-phi-silica` | `--prepare-phi-silica` (downloads the model) |
|
||||
|----------------------|----------------------------------------------|
|
||||
| `0` Available · `1` NotReady · `2` NotSupported / unlock failed | `0` Ready · `1` Failed · `2` NotSupported |
|
||||
|
||||
`--check` only reads state; use `--prepare` to trigger the model download (`EnsureReadyAsync`).
|
||||
On failure it prints the `HRESULT` to stderr.
|
||||
|
||||
Confirm the running AP has identity:
|
||||
|
||||
```powershell
|
||||
$apPid = (Get-Process PowerToys.AdvancedPaste -EA SilentlyContinue | Select-Object -First 1).Id
|
||||
if ($apPid) { & "$repo\src\PackageIdentity\Check-ProcessIdentity.ps1" -ProcessId $apPid }
|
||||
# Expect a PFN ending in the publisher id that matches the baked attestation
|
||||
```
|
||||
|
||||
## What actually breaks it
|
||||
|
||||
- **Missing `.winmd` (most important).** The Windows AI runtime resolves
|
||||
`Microsoft.Windows.AI*.winmd` from the app folder at runtime. If they aren't deployed,
|
||||
`GetReadyState()` returns `NotReady` and `EnsureReadyAsync()` fails with
|
||||
`RO_E_METADATA_NAME_NOT_FOUND` (`0x8000000F`) — even though identity, token, and the AI DLLs
|
||||
are all correct. The build emits these winmd into `WinUI3Apps\`; the **installer must harvest
|
||||
them** (`*.winmd` is in the inclusion list of
|
||||
[`generateAllFileComponents.ps1`](../../../installer/PowerToysSetupVNext/generateAllFileComponents.ps1)).
|
||||
Classic symptom: "works from the build output but not from the installer" → check that the
|
||||
installed `WinUI3Apps\` contains `Microsoft.Windows.AI*.winmd`.
|
||||
- **Dev/prod mismatch.** A dev-cred exe running against a prod sparse package (or vice versa)
|
||||
makes the LAF unlock silently return `Unavailable`. Keep the exe and the registered package
|
||||
the same flavor, and verify with the attestation == publisherId check above.
|
||||
- **Forgot to redirect.** `--check-phi-silica` in a console prints nothing — that's the
|
||||
GUI-subsystem quirk, not a result.
|
||||
|
||||
## Cleanup
|
||||
|
||||
```powershell
|
||||
pwsh -ExecutionPolicy Bypass -File "$repo\src\PackageIdentity\BuildSparsePackage.ps1" -Unregister
|
||||
```
|
||||
|
||||
⚠️ This removes any `Microsoft.PowerToys.SparseApp` registration, **including a prod one** from
|
||||
an installer — reinstall/repair PowerToys to restore it.
|
||||
@@ -33,81 +33,7 @@ See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `Opt
|
||||
|
||||
## Debugging
|
||||
|
||||
Advanced Paste is an unpackaged, self-contained WinUI 3 app (`PowerToys.AdvancedPaste.exe`). To call Windows AI APIs (Phi Silica / `Microsoft.Windows.AI.Text.LanguageModel`) it acquires **package identity** at runtime via a shared sparse MSIX package (`Microsoft.PowerToys.SparseApp`).
|
||||
|
||||
### Running and attaching the debugger
|
||||
|
||||
1. Set the **Runner** project (`src/runner`) as the startup project in Visual Studio.
|
||||
2. Launch the Runner (F5). This starts the PowerToys tray icon and loads all module interfaces.
|
||||
3. Open Settings (right-click tray icon → Settings) and enable the **Advanced Paste** module if it isn't already. The module launches `PowerToys.AdvancedPaste.exe` in the background immediately.
|
||||
4. In Visual Studio, go to **Debug → Attach to Process** (`Ctrl+Alt+P`) and attach to `PowerToys.AdvancedPaste.exe` (select **Managed (.NET Core)** debugger).
|
||||
|
||||
Alternatively, use the VS Code launch configuration **"Run AdvancedPaste"** from [.vscode/launch.json](/.vscode/launch.json) to launch the exe directly — but note that without the Runner, IPC and hotkeys won't work.
|
||||
|
||||
### Sparse package identity (local development)
|
||||
|
||||
#### Why is this needed?
|
||||
|
||||
- The `LanguageModel` API requires a Limited Access Feature (LAF) unlock, which only succeeds when the calling process has a matching package identity.
|
||||
- Advanced Paste is an unpackaged, self-contained WinUI 3 app. The sparse package grants it identity without converting it to a full MSIX.
|
||||
- The csproj uses `<ProjectPriFileName>PowerToys.AdvancedPaste.pri</ProjectPriFileName>` (matching the convention of other WinUI3 apps like ImageResizer). This requires WindowsAppSDK Foundation >= 2.0.22 ([PR #6376](https://github.com/microsoft/WindowsAppSDK/pull/6376)) which fixes MRT PRI lookup under sparse identity so `Application.LoadComponent` resolves custom-named PRI files instead of hard-coding `resources.pri`.
|
||||
|
||||
#### One-step dev setup
|
||||
|
||||
```powershell
|
||||
pwsh src/PackageIdentity/BuildSparsePackage.ps1 -Platform ARM64 -Configuration Debug -DevRegister
|
||||
```
|
||||
|
||||
`-DevRegister`:
|
||||
1. Generates a dev certificate under `src/PackageIdentity/.user/` (first run only).
|
||||
2. Auto-imports that certificate into `CurrentUser\TrustedPeople` and `CurrentUser\Root` so the OS grants sparse identity to AP (without trust, `GetPackageFamilyName` returns `APPMODEL_ERROR_NO_PACKAGE` and LAF unlock silently fails).
|
||||
3. Removes any prior registration.
|
||||
4. Rewrites the publisher in a temp copy of `AppxManifest.xml` to match the dev cert subject.
|
||||
5. Registers via `Add-AppxPackage -Register … -ExternalLocation X:\…\<Platform>\<Config>\WinUI3Apps`.
|
||||
|
||||
After registration verify:
|
||||
|
||||
```powershell
|
||||
$pkg = Get-AppxPackage -Name '*SparseApp*'
|
||||
$pkg.PackageFamilyName # Microsoft.PowerToys.SparseApp_<PublisherId>
|
||||
$pkg.PublisherId # djwsxzxb4ksa8
|
||||
$pkg.IsDevelopmentMode # True
|
||||
```
|
||||
|
||||
Confirm AP picks up sparse identity at runtime:
|
||||
|
||||
```powershell
|
||||
& 'ARM64\Debug\WinUI3Apps\PowerToys.AdvancedPaste.exe' --check-phi-silica
|
||||
# Exit 0 = Available, 1 = NotReady, 2 = NotSupported
|
||||
```
|
||||
|
||||
Re-register after rebuilding AP, changing `src/PackageIdentity/AppxManifest.xml`, or switching platforms/configurations by re-running the same command. Unregister with `-Unregister`.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `GetPackageFamilyName` returns `APPMODEL_ERROR_NO_PACKAGE` (15700) at runtime; LAF unlock returns `Unavailable` | Dev certificate not trusted (or sparse package not registered) | Re-run `BuildSparsePackage.ps1 -DevRegister` — auto-imports the cert into `TrustedPeople` and `Root`. |
|
||||
| `Microsoft.UI.Xaml.dll` crash with `0xC000027B` (class-not-registered) on AP or Settings startup | `<Application>` `Executable` path in `src/PackageIdentity/AppxManifest.xml` does not resolve under the registered `ExternalLocation` (`<Config>\WinUI3Apps\`) | Confirm every `Executable` is relative to `WinUI3Apps\` (per #47177) and the file exists under the build output. |
|
||||
| AP launches but never shows a window when triggered via hotkey | Runner's pipe-server wait timed out before AP's cold-start finished bootstrapping WinAppSDK + DI host | Already mitigated by the 15 s pipe timeout in `AdvancedPasteProcessManager.cpp`; warm-start launches connect in well under 1 s. |
|
||||
| `XamlParseException` / `ms-appx:///Microsoft.UI.Xaml/Themes/…` not found | WindowsAppSDK Foundation < 2.0.22; MRT can't resolve custom PRI name under sparse identity | Ensure `Microsoft.WindowsAppSDK.Foundation` >= 2.0.22 in `Directory.Packages.props`. |
|
||||
|
||||
### How Settings UI checks Phi Silica availability
|
||||
|
||||
Settings UI does not have sparse package identity. To check whether Phi Silica is available, it launches Advanced Paste as a short-lived subprocess:
|
||||
|
||||
```
|
||||
PowerToys.AdvancedPaste.exe --check-phi-silica
|
||||
```
|
||||
|
||||
`Program.Main` recognizes this flag, calls `PhiSilicaLafHelper.TryUnlock()` + `LanguageModel.GetReadyState()`, prints one of `Available` / `NotReady` / `NotSupported` to stdout, and exits with the matching code (0/1/2). Settings reads stdout with a 10 s wait. Because each call is a fresh process, transient `Unavailable` results are not cached across checks.
|
||||
|
||||
### See also
|
||||
|
||||
- [Phi Silica local testing & troubleshooting guide](advancedpaste-phisilica-local-testing.md) — layer-by-layer diagnostics for Phi Silica availability
|
||||
- [`src/PackageIdentity/readme.md`](/src/PackageIdentity/readme.md) — full sparse package documentation
|
||||
- [microsoft/microsoft-ui-xaml#10856](https://github.com/microsoft/microsoft-ui-xaml/issues/10856) — original WinUI sparse-identity PRI bug
|
||||
- [microsoft/WindowsAppSDK#6376](https://github.com/microsoft/WindowsAppSDK/pull/6376) — MRT sparse PRI fix (Foundation >= 2.0.22)
|
||||
TODO: Add debugging information
|
||||
|
||||
## Settings
|
||||
|
||||
|
||||
@@ -79,4 +79,4 @@ Below are community created plugins that target a website or software. They are
|
||||
| [Linear](https://github.com/vednig/powertoys-linear) | [vednig](https://github.com/vednig) | Create Linear Issues directly from Powertoys Run |
|
||||
| [PerplexitySearchShortcut](https://github.com/0x6f677548/PowerToys-Run-PerplexitySearchShortcut) | [0x6f677548](https://github.com/0x6f677548) | Search Perplexity |
|
||||
| [SpeedTest](https://github.com/ruslanlap/PowerToysRun-SpeedTest) | [ruslanlap](https://github.com/ruslanlap) | One-command internet speed tests with real-time results, modern UI, and shareable links. |
|
||||
| [DiskAnalyzer](https://github.com/valley-soft/powertoys-diskanalyzer) | [ValleySoft](https://github.com/valley-soft) | Scan folders, find the largest files, and view drive space usage. |
|
||||
| [DiskAnalyzer](https://github.com/thetsaw/PowerToys.Plugin) | [thetsaw](https://github.com/thetsaw) | Scan folders, find the largest files, and view drive space usage with visual progress bars. |
|
||||
|
||||
@@ -28,23 +28,12 @@ Function Generate-FileList() {
|
||||
|
||||
$fileExclusionList = @("*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe", "powertoys.exe")
|
||||
|
||||
# *.winmd: WinRT metadata for the Windows App SDK AI APIs (Phi Silica, Imaging, etc.). The AI
|
||||
# runtime resolves these from the app directory at runtime, so they must ship with the product.
|
||||
# Without them GetReadyState() reports NotReady and EnsureReadyAsync() fails with
|
||||
# RO_E_METADATA_NAME_NOT_FOUND (0x8000000F). The build already emits them into the app output
|
||||
# (e.g. WinUI3Apps); they were previously dropped here because the harvest didn't include them.
|
||||
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri", "*.yml", "*.winmd")
|
||||
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri", "*.yml")
|
||||
|
||||
# MFC DLLs leak into the output via WindowsAppSDKSelfContained but no PowerToys binary imports them.
|
||||
# Verified with dumpbin /dependents across all 2176 binaries — zero consumers.
|
||||
$fileExclusionList += @("mfc140.dll", "mfc140u.dll", "mfcm140.dll", "mfcm140u.dll")
|
||||
|
||||
# Microsoft.CommandPalette.Extensions.winmd already has a dedicated WiX component
|
||||
# (Microsoft_CommandPalette_Extensions_winmd in BaseApplications.wxs, placed in WinUI3Apps for
|
||||
# CmdPal's WinRT resolution). Exclude it from the generic *.winmd harvest so it isn't declared
|
||||
# by two components (WIX ICE30 "installed by two different components" breaks ref-counting).
|
||||
$fileExclusionList += @("Microsoft.CommandPalette.Extensions.winmd")
|
||||
|
||||
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")
|
||||
|
||||
if ($fileDepsJson -eq [string]::Empty) {
|
||||
|
||||
@@ -58,16 +58,6 @@
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.AdvancedPasteUI" Executable="PowerToys.AdvancedPaste.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.AdvancedPaste"
|
||||
Description="PowerToys Advanced Paste"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.CmdPalExtension" Executable="Microsoft.CmdPal.Ext.PowerToys.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.CommandPaletteExtension"
|
||||
|
||||
@@ -13,9 +13,7 @@ Param(
|
||||
[switch]$Clean,
|
||||
[switch]$ForceCert,
|
||||
[switch]$NoSign,
|
||||
[switch]$CIBuild,
|
||||
[switch]$DevRegister,
|
||||
[switch]$Unregister
|
||||
[switch]$CIBuild
|
||||
)
|
||||
|
||||
# PowerToys sparse packaging helper.
|
||||
@@ -24,19 +22,6 @@ Param(
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Handle -Unregister early exit
|
||||
if ($Unregister) {
|
||||
$existing = Get-AppxPackage -Name 'Microsoft.PowerToys.SparseApp' -ErrorAction SilentlyContinue
|
||||
if ($existing) {
|
||||
Write-Host "Removing Microsoft.PowerToys.SparseApp..."
|
||||
$existing | Remove-AppxPackage
|
||||
Write-Host "Done."
|
||||
} else {
|
||||
Write-Host "Microsoft.PowerToys.SparseApp is not registered."
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
$isCIBuild = $false
|
||||
if ($CIBuild.IsPresent) {
|
||||
$isCIBuild = $true
|
||||
@@ -436,85 +421,3 @@ $winUI3AppsDir = Join-Path $outDir "WinUI3Apps"
|
||||
Write-BuildLog "Register sparse package:" -Level Info
|
||||
Write-BuildLog " Add-AppxPackage -Path `"$msixPath`" -ExternalLocation `"$winUI3AppsDir`"" -Level Warning
|
||||
Write-BuildLog "(If already installed and you changed manifest only): Add-AppxPackage -Register `"$manifestPath`" -ExternalLocation `"$winUI3AppsDir`" -ForceApplicationShutdown" -Level Warning
|
||||
|
||||
# -DevRegister: automatically register the sparse package for local development
|
||||
if ($DevRegister) {
|
||||
Write-BuildLog "`nDev registration requested..." -Level Info
|
||||
|
||||
# Ensure the dev certificate is trusted (CurrentUser\TrustedPeople + CurrentUser\Root).
|
||||
# Without trust, sparse identity lookup silently fails at runtime (GetPackageFamilyName
|
||||
# returns APPMODEL_ERROR_NO_PACKAGE) so LAF unlocks like Phi Silica return Unavailable.
|
||||
if (Test-Path $CertCerFile) {
|
||||
try {
|
||||
$devCert = (Get-PfxCertificate -FilePath $CertCerFile -ErrorAction Stop)
|
||||
$devThumbprint = $devCert.Thumbprint
|
||||
foreach ($store in @('TrustedPeople', 'Root')) {
|
||||
$storePath = "Cert:\CurrentUser\$store"
|
||||
$present = Get-ChildItem $storePath -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Thumbprint -eq $devThumbprint }
|
||||
if (-not $present) {
|
||||
Write-BuildLog "Importing dev certificate ($devThumbprint) into CurrentUser\$store" -Level Info
|
||||
Import-Certificate -FilePath $CertCerFile -CertStoreLocation $storePath | Out-Null
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-BuildLog "Failed to import dev certificate into trust stores: $($_.Exception.Message)" -Level Warning
|
||||
Write-BuildLog "Sparse identity may not be granted at runtime; LAF unlocks (e.g., Phi Silica) will fail." -Level Warning
|
||||
}
|
||||
} else {
|
||||
Write-BuildLog "Certificate .cer file not found at $CertCerFile; skipping trust-store import." -Level Warning
|
||||
}
|
||||
|
||||
# Remove existing registration
|
||||
$existing = Get-AppxPackage -Name $script:Config.IdentityName -ErrorAction SilentlyContinue
|
||||
if ($existing) {
|
||||
Write-BuildLog "Removing existing registration..." -Level Info
|
||||
$existing | Remove-AppxPackage
|
||||
}
|
||||
|
||||
# Create a temp manifest with the dev publisher for -Register
|
||||
$devRegDir = Join-Path ([System.IO.Path]::GetTempPath()) "PowerToysSparseDevReg"
|
||||
if (Test-Path $devRegDir) { Remove-Item $devRegDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $devRegDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
Copy-Item $manifestPath (Join-Path $devRegDir 'AppxManifest.xml')
|
||||
$imagesDir = Join-Path $sparseDir 'Images'
|
||||
if (Test-Path $imagesDir) {
|
||||
Copy-Item $imagesDir (Join-Path $devRegDir 'Images') -Recurse
|
||||
}
|
||||
|
||||
$devManifest = Join-Path $devRegDir 'AppxManifest.xml'
|
||||
[xml]$devXml = Get-Content $devManifest -Raw
|
||||
$devIdentity = $devXml.Package.Identity
|
||||
|
||||
if ($devIdentity.Publisher -ne $script:Config.CertSubject) {
|
||||
Write-BuildLog "Rewriting publisher for dev registration" -Level Info
|
||||
$devIdentity.SetAttribute('Publisher', $script:Config.CertSubject)
|
||||
$devXml.Save($devManifest)
|
||||
}
|
||||
|
||||
Write-BuildLog "Registering with ExternalLocation: $winUI3AppsDir" -Level Info
|
||||
Add-AppxPackage -Register $devManifest -ExternalLocation $winUI3AppsDir
|
||||
|
||||
$pkg = Get-AppxPackage -Name $script:Config.IdentityName -ErrorAction SilentlyContinue
|
||||
if ($pkg) {
|
||||
Write-BuildLog "Dev registration successful:" -Level Success
|
||||
Write-BuildLog " Publisher: $($pkg.Publisher)" -Level Info
|
||||
Write-BuildLog " PublisherId: $($pkg.PublisherId)" -Level Info
|
||||
Write-BuildLog " IsDevelopmentMode: $($pkg.IsDevelopmentMode)" -Level Info
|
||||
Write-BuildLog " InstallLocation: $($pkg.InstallLocation)" -Level Info
|
||||
|
||||
if ($pkg.PublisherId -ne 'djwsxzxb4ksa8') {
|
||||
Write-BuildLog "PublisherId mismatch! Expected 'djwsxzxb4ksa8', got '$($pkg.PublisherId)'. LAF unlock will fail." -Level Warning
|
||||
}
|
||||
} else {
|
||||
Write-BuildLog "Dev registration failed — package not found." -Level Error
|
||||
exit 1
|
||||
}
|
||||
} finally {
|
||||
if (Test-Path $devRegDir) {
|
||||
Remove-Item $devRegDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!-- Phi Silica Limited Access Feature credentials.
|
||||
Local dev defaults below; overridden by /p: in release pipelines. -->
|
||||
<PhiSilicaLafToken Condition="'$(PhiSilicaLafToken)'==''">RmToMMYJHZkQSrKP5lWesA==</PhiSilicaLafToken>
|
||||
<PhiSilicaLafAttestation Condition="'$(PhiSilicaLafAttestation)'==''">djwsxzxb4ksa8</PhiSilicaLafAttestation>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -55,22 +55,6 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;
|
||||
|
||||
public string FixSpellingAndGrammarPrompt => string.Empty;
|
||||
|
||||
public string FixSpellingAndGrammarSystemPrompt => string.Empty;
|
||||
|
||||
public string FixSpellingAndGrammarProviderId => string.Empty;
|
||||
|
||||
public bool FixSpellingAndGrammarCoachingEnabled => false;
|
||||
|
||||
public bool FixSpellingAndGrammarCoachingShortcutSet => false;
|
||||
|
||||
public string FixSpellingAndGrammarCoachingPrompt => string.Empty;
|
||||
|
||||
public string FixSpellingAndGrammarCoachingSystemPrompt => string.Empty;
|
||||
|
||||
public string FixSpellingAndGrammarCoachingProviderId => string.Empty;
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration => _configuration;
|
||||
|
||||
public event EventHandler Changed;
|
||||
|
||||
@@ -30,16 +30,6 @@ public sealed class CustomActionKernelQueryCacheServiceTests
|
||||
private static readonly CacheValue TestValue = new([new(PasteFormats.PlainText, [])]);
|
||||
private static readonly CacheValue TestValue2 = new([new(PasteFormats.KernelQuery, new() { { "a", "b" }, { "c", "d" } })]);
|
||||
|
||||
private static string LocalizeResourceId(string resourceId) => resourceId switch
|
||||
{
|
||||
"PasteAsPlainText" => "Paste as plain text",
|
||||
"PasteAsMarkdown" => MarkdownTestKey.Prompt,
|
||||
"PasteAsJson" => JSONTestKey.Prompt,
|
||||
"PasteAsTxtFile" => PasteAsTxtFileKey.Prompt,
|
||||
"PasteAsPngFile" => PasteAsPngFileKey.Prompt,
|
||||
_ => resourceId,
|
||||
};
|
||||
|
||||
private CustomActionKernelQueryCacheService _cacheService;
|
||||
private Mock<IUserSettings> _userSettings;
|
||||
private MockFileSystem _fileSystem;
|
||||
@@ -51,7 +41,7 @@ public sealed class CustomActionKernelQueryCacheServiceTests
|
||||
UpdateUserActions([], []);
|
||||
|
||||
_fileSystem = new();
|
||||
_cacheService = new(_userSettings.Object, _fileSystem, LocalizeResourceId);
|
||||
_cacheService = new(_userSettings.Object, _fileSystem);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -132,7 +122,7 @@ public sealed class CustomActionKernelQueryCacheServiceTests
|
||||
await _cacheService.WriteAsync(JSONTestKey, TestValue);
|
||||
await _cacheService.WriteAsync(MarkdownTestKey, TestValue2);
|
||||
|
||||
_cacheService = new(_userSettings.Object, _fileSystem, LocalizeResourceId); // recreate using same mock file-system to simulate app restart
|
||||
_cacheService = new(_userSettings.Object, _fileSystem); // recreate using same mock file-system to simulate app restart
|
||||
|
||||
AssertAreEqual(TestValue, _cacheService.ReadOrNull(JSONTestKey));
|
||||
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(MarkdownTestKey));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\CmdPalVersion.props" />
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
@@ -9,7 +8,7 @@
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<ApplicationIcon>Assets\AdvancedPaste\AdvancedPaste.ico</ApplicationIcon>
|
||||
<ApplicationManifest>AdvancedPaste.dev.manifest</ApplicationManifest>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
@@ -21,13 +20,9 @@
|
||||
<RootNamespace>AdvancedPaste</RootNamespace>
|
||||
<DisableWinExeOutputInference>true</DisableWinExeOutputInference>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>PowerToys.AdvancedPaste.pri</ProjectPriFileName>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
<Version>$(AdvancedPasteVersion)</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ApplicationManifest>AdvancedPaste.prod.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
|
||||
@@ -37,25 +32,6 @@
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- BODGY: XES Versioning and WinAppSDK get into a fight about the app manifest, which breaks WinAppSDK. -->
|
||||
<Target Name="RearrangeXefVersioningAndWinAppSDKResourceGeneration" DependsOnTargets="GetNewAppManifestValues;CreateWinRTRegistration" BeforeTargets="XesWriteVersionInfoResourceFile">
|
||||
<PropertyGroup>
|
||||
<OriginalApplicationManifest>$(ApplicationManifest.Replace("$(MSBuildProjectDirectory)\",""))</OriginalApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<Message Importance="High" Text="Updated final manifest path to $(OriginalApplicationManifest)" />
|
||||
</Target>
|
||||
|
||||
<!-- Generate PhiSilicaLafCredentials from MSBuild properties (PhiSilicaLaf.props / CI overrides). -->
|
||||
<Target Name="GeneratePhiSilicaLafCredentials" BeforeTargets="BeforeCompile;CoreCompile;XamlPreCompile">
|
||||
<PropertyGroup>
|
||||
<PhiSilicaLafCredentialsFile>$(IntermediateOutputPath)PhiSilicaLafCredentials.g.cs</PhiSilicaLafCredentialsFile>
|
||||
</PropertyGroup>
|
||||
<WriteLinesToFile File="$(PhiSilicaLafCredentialsFile)" Overwrite="true" Lines="// <auto-generated/>;namespace AdvancedPaste%3B;;internal static class PhiSilicaLafCredentials;{; internal const string Token = "$(PhiSilicaLafToken)"%3B; internal const string Attestation = "$(PhiSilicaLafAttestation)"%3B;}" />
|
||||
<ItemGroup>
|
||||
<Compile Include="$(PhiSilicaLafCredentialsFile)" Condition="!@(Compile->AnyHaveMetadataValue('Identity', '$(PhiSilicaLafCredentialsFile)'))" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="AdvancedPasteXAML\Controls\ClipboardHistoryItemPreviewControl.xaml" />
|
||||
<None Remove="AdvancedPasteXAML\Controls\PromptBox.xaml" />
|
||||
@@ -90,9 +66,9 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.ClientModel" />
|
||||
<PackageReference Include="Microsoft.Windows.Compatibility" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="ReverseMarkdown" />
|
||||
@@ -109,8 +85,7 @@
|
||||
<!-- TODO: fix issues and reenable -->
|
||||
<!-- These are caused by streamjsonrpc dependency on Microsoft.VisualStudio.Threading.Analyzers -->
|
||||
<!-- We might want to add that to the project and fix the issues as well -->
|
||||
<!-- CS8305: generated XamlTypeInfo references experimental ItemsView enums in WinAppSDK 2.2.x-experimental. -->
|
||||
<NoWarn>VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101;CS8305</NoWarn>
|
||||
<NoWarn>VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
@@ -122,10 +97,6 @@
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SDKReference Include="Microsoft.VCLibs.Desktop, Version=14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>AdvancedPaste.UnitTests</_Parameter1>
|
||||
@@ -133,6 +104,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
@@ -181,5 +154,4 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0"
|
||||
xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="AdvancedPaste.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- The combination of below two tags have the following effect:
|
||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
||||
publisher="CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US"
|
||||
packageName="Microsoft.PowerToys.SparseApp"
|
||||
applicationId="PowerToys.AdvancedPasteUI" />
|
||||
</assembly>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0"
|
||||
xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="AdvancedPaste.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- The combination of below two tags have the following effect:
|
||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
||||
publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
packageName="Microsoft.PowerToys.SparseApp"
|
||||
applicationId="PowerToys.AdvancedPasteUI" />
|
||||
</assembly>
|
||||
@@ -188,23 +188,14 @@ namespace AdvancedPaste
|
||||
}
|
||||
else
|
||||
{
|
||||
const string coachingSuffix = "-coaching";
|
||||
var actionKey = messageParts[1];
|
||||
bool forceCoaching = actionKey.EndsWith(coachingSuffix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (forceCoaching)
|
||||
{
|
||||
actionKey = actionKey[..^coachingSuffix.Length];
|
||||
}
|
||||
|
||||
if (!AdditionalActionIPCKeys.TryGetValue(actionKey, out PasteFormats pasteFormat))
|
||||
if (!AdditionalActionIPCKeys.TryGetValue(messageParts[1], out PasteFormats pasteFormat))
|
||||
{
|
||||
Logger.LogWarning($"Unexpected additional action type {messageParts[1]}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowWindow();
|
||||
await viewModel.ExecutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut, forceCoaching);
|
||||
await viewModel.ExecutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,7 +382,6 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid
|
||||
@@ -439,32 +438,13 @@
|
||||
TextWrapping="Wrap" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Padding="12,8,12,8"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
Spacing="4"
|
||||
Visibility="{x:Bind ViewModel.HasCoachingExplanation, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<TextBlock
|
||||
x:Uid="CoachingExplanationTitle"
|
||||
FontWeight="SemiBold"
|
||||
Style="{StaticResource CaptionTextBlockStyle}" />
|
||||
<ScrollViewer MaxHeight="200">
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
IsTextSelectionEnabled="True"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.CoachingExplanation, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
<Rectangle
|
||||
Grid.Row="2"
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<Grid
|
||||
Grid.Row="3"
|
||||
Grid.Row="2"
|
||||
Margin="12"
|
||||
ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
|
||||
@@ -25,22 +25,6 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions { get; }
|
||||
|
||||
public string FixSpellingAndGrammarPrompt { get; }
|
||||
|
||||
public string FixSpellingAndGrammarSystemPrompt { get; }
|
||||
|
||||
public string FixSpellingAndGrammarProviderId { get; }
|
||||
|
||||
public bool FixSpellingAndGrammarCoachingEnabled { get; }
|
||||
|
||||
public bool FixSpellingAndGrammarCoachingShortcutSet { get; }
|
||||
|
||||
public string FixSpellingAndGrammarCoachingPrompt { get; }
|
||||
|
||||
public string FixSpellingAndGrammarCoachingSystemPrompt { get; }
|
||||
|
||||
public string FixSpellingAndGrammarCoachingProviderId { get; }
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; }
|
||||
|
||||
public event EventHandler Changed;
|
||||
|
||||
@@ -157,6 +157,8 @@ namespace AdvancedPaste.Helpers
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
|
||||
public static explicit operator System.Windows.Point(PointInter point) => new System.Windows.Point(point.X, point.Y);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
|
||||
@@ -46,22 +46,6 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
|
||||
public string FixSpellingAndGrammarPrompt { get; private set; } = string.Empty;
|
||||
|
||||
public string FixSpellingAndGrammarSystemPrompt { get; private set; } = string.Empty;
|
||||
|
||||
public string FixSpellingAndGrammarProviderId { get; private set; } = string.Empty;
|
||||
|
||||
public bool FixSpellingAndGrammarCoachingEnabled { get; private set; }
|
||||
|
||||
public bool FixSpellingAndGrammarCoachingShortcutSet { get; private set; }
|
||||
|
||||
public string FixSpellingAndGrammarCoachingPrompt { get; private set; } = string.Empty;
|
||||
|
||||
public string FixSpellingAndGrammarCoachingSystemPrompt { get; private set; } = string.Empty;
|
||||
|
||||
public string FixSpellingAndGrammarCoachingProviderId { get; private set; } = string.Empty;
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; private set; }
|
||||
|
||||
public UserSettings(IFileSystem fileSystem)
|
||||
@@ -129,21 +113,10 @@ namespace AdvancedPaste.Settings
|
||||
EnableClipboardPreview = properties.EnableClipboardPreview;
|
||||
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
|
||||
|
||||
var fixSpellingAction = properties.AdditionalActions.FixSpellingAndGrammar;
|
||||
FixSpellingAndGrammarPrompt = fixSpellingAction.Prompt ?? string.Empty;
|
||||
FixSpellingAndGrammarSystemPrompt = fixSpellingAction.SystemPrompt ?? string.Empty;
|
||||
FixSpellingAndGrammarProviderId = fixSpellingAction.ProviderId ?? string.Empty;
|
||||
FixSpellingAndGrammarCoachingEnabled = fixSpellingAction.CoachingEnabled;
|
||||
FixSpellingAndGrammarCoachingShortcutSet = fixSpellingAction.CoachingShortcut?.Code > 0;
|
||||
FixSpellingAndGrammarCoachingPrompt = fixSpellingAction.CoachingPrompt ?? string.Empty;
|
||||
FixSpellingAndGrammarCoachingSystemPrompt = fixSpellingAction.CoachingSystemPrompt ?? string.Empty;
|
||||
FixSpellingAndGrammarCoachingProviderId = fixSpellingAction.CoachingProviderId ?? string.Empty;
|
||||
|
||||
var sourceAdditionalActions = properties.AdditionalActions;
|
||||
(PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats =
|
||||
[
|
||||
(PasteFormats.ImageToText, [sourceAdditionalActions.ImageToText]),
|
||||
(PasteFormats.FixSpellingAndGrammar, [sourceAdditionalActions.FixSpellingAndGrammar]),
|
||||
(PasteFormats.PasteAsTxtFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsTxtFile]),
|
||||
(PasteFormats.PasteAsPngFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsPngFile]),
|
||||
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile]),
|
||||
|
||||
@@ -24,22 +24,20 @@ public sealed class PasteFormat
|
||||
IsEnabled = SupportsClipboardFormats(clipboardFormats) && (isAIServiceEnabled || !Metadata.RequiresAIService);
|
||||
}
|
||||
|
||||
public static PasteFormat CreateStandardFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func<string, string> resourceLoader, string providerId = null) =>
|
||||
public static PasteFormat CreateStandardFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func<string, string> resourceLoader) =>
|
||||
new(format, clipboardFormats, isAIServiceEnabled)
|
||||
{
|
||||
Name = MetadataDict[format].ResourceId == null ? string.Empty : resourceLoader(MetadataDict[format].ResourceId),
|
||||
Prompt = string.Empty,
|
||||
IsSavedQuery = false,
|
||||
ProviderId = providerId ?? string.Empty,
|
||||
};
|
||||
|
||||
public static PasteFormat CreateCustomAIFormat(PasteFormats format, string name, string prompt, bool isSavedQuery, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, string providerId = null) =>
|
||||
public static PasteFormat CreateCustomAIFormat(PasteFormats format, string name, string prompt, bool isSavedQuery, ClipboardFormat clipboardFormats, bool isAIServiceEnabled) =>
|
||||
new(format, clipboardFormats, isAIServiceEnabled)
|
||||
{
|
||||
Name = name,
|
||||
Prompt = prompt,
|
||||
IsSavedQuery = isSavedQuery,
|
||||
ProviderId = providerId ?? string.Empty,
|
||||
};
|
||||
|
||||
public PasteFormatMetadataAttribute Metadata => MetadataDict[Format];
|
||||
@@ -52,8 +50,6 @@ public sealed class PasteFormat
|
||||
|
||||
public string Prompt { get; private init; }
|
||||
|
||||
public string ProviderId { get; private init; } = string.Empty;
|
||||
|
||||
public bool IsSavedQuery { get; private init; }
|
||||
|
||||
public bool IsEnabled { get; private init; }
|
||||
|
||||
@@ -38,17 +38,6 @@ public enum PasteFormats
|
||||
KernelFunctionDescription = "Takes clipboard text and formats it as JSON text.")]
|
||||
Json,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
IsCoreAction = false,
|
||||
ResourceId = "FixSpellingAndGrammar",
|
||||
IconGlyph = "\uE8E2",
|
||||
RequiresAIService = true,
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Text,
|
||||
IPCKey = AdvancedPasteAdditionalActions.PropertyNames.FixSpellingAndGrammar,
|
||||
KernelFunctionDescription = "Fixes all spelling and grammar errors in the clipboard text and returns the corrected version.")]
|
||||
FixSpellingAndGrammar,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
IsCoreAction = false,
|
||||
ResourceId = "ImageToText",
|
||||
|
||||
@@ -1,67 +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.Diagnostics;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace AdvancedPaste;
|
||||
|
||||
internal static class PhiSilicaLafHelper
|
||||
{
|
||||
private const string FeatureId = "com.microsoft.windows.ai.languagemodel";
|
||||
|
||||
private static readonly object _lock = new();
|
||||
private static bool _unlocked;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of the most recent <see cref="TryUnlock"/> attempt
|
||||
/// (e.g. Available, AvailableWithoutToken, Unavailable, or "Exception: ...").
|
||||
/// Exposed so callers can surface the real LAF result for diagnostics; the
|
||||
/// generic "Access is denied" from downstream model calls does not reveal it.
|
||||
/// </summary>
|
||||
public static string LastUnlockStatus { get; private set; } = "NotAttempted";
|
||||
|
||||
public static bool TryUnlock()
|
||||
{
|
||||
// Only cache a successful unlock. Negative results (Unavailable, Unknown, exceptions)
|
||||
// are often transient — e.g., AI feature stack not yet initialized after sign-in or
|
||||
// sparse identity not fully applied to a freshly-started process — and retrying on
|
||||
// the next call lets AP recover without restart.
|
||||
if (_unlocked)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_unlocked)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var access = LimitedAccessFeatures.TryUnlockFeature(
|
||||
FeatureId,
|
||||
PhiSilicaLafCredentials.Token,
|
||||
PhiSilicaLafCredentials.Attestation + " has registered their use of com.microsoft.windows.ai.languagemodel with Microsoft and agrees to the terms of use.");
|
||||
|
||||
_unlocked = access.Status == LimitedAccessFeatureStatus.Available
|
||||
|| access.Status == LimitedAccessFeatureStatus.AvailableWithoutToken;
|
||||
|
||||
LastUnlockStatus = access.Status.ToString();
|
||||
Debug.WriteLine($"Phi Silica LAF unlock status: {access.Status}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastUnlockStatus = "Exception: " + ex.Message;
|
||||
Debug.WriteLine($"Phi Silica LAF unlock failed: {ex.Message}");
|
||||
_unlocked = false;
|
||||
}
|
||||
|
||||
return _unlocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
using ManagedCommon;
|
||||
@@ -15,26 +14,16 @@ namespace AdvancedPaste
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger("\\AdvancedPaste\\Logs");
|
||||
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
if (args.Contains("--check-phi-silica", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return CheckPhiSilicaAvailability();
|
||||
}
|
||||
|
||||
if (args.Contains("--prepare-phi-silica", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return PreparePhiSilica();
|
||||
}
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAdvancedPasteEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
return 1;
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_AdvancedPaste_Instance");
|
||||
@@ -52,100 +41,6 @@ namespace AdvancedPaste
|
||||
{
|
||||
Logger.LogWarning("Another instance of AdvancedPasteUI is running. Exiting.");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks Phi Silica availability without starting the WinUI app.
|
||||
/// Used by Settings UI to probe API status via subprocess.
|
||||
/// Exit codes: 0 = available, 1 = not ready (model needs download), 2 = not supported or error.
|
||||
/// </summary>
|
||||
private static int CheckPhiSilicaAvailability()
|
||||
{
|
||||
try
|
||||
{
|
||||
PhiSilicaLafHelper.TryUnlock();
|
||||
var readyState = Microsoft.Windows.AI.Text.LanguageModel.GetReadyState();
|
||||
|
||||
Console.Error.WriteLine($"[phi-silica] LAF unlock status: {PhiSilicaLafHelper.LastUnlockStatus}; ReadyState: {readyState}");
|
||||
|
||||
switch (readyState)
|
||||
{
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.Ready:
|
||||
Console.Out.WriteLine("Available");
|
||||
return 0;
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotReady:
|
||||
Console.Out.WriteLine("NotReady");
|
||||
return 1;
|
||||
default:
|
||||
// NotSupportedOnCurrentSystem, DisabledByUser, CapabilityMissing,
|
||||
// NotCompatibleWithSystemHardware, OSUpdateNeeded, or any future state:
|
||||
// the model isn't usable and "Download model" (EnsureReadyAsync) won't fix it.
|
||||
// CapabilityMissing in particular means the systemAIModels capability isn't
|
||||
// authorized for the app, so EnsureReadyAsync throws E_ACCESSDENIED (0x80070005).
|
||||
Console.Out.WriteLine("NotSupported");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
Console.Out.WriteLine("NotSupported");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers Phi Silica model preparation (download) without starting the WinUI app.
|
||||
/// Moves the model from NotReady to Ready by calling EnsureReadyAsync.
|
||||
/// Exit codes: 0 = ready, 1 = preparation failed, 2 = not supported or error.
|
||||
/// </summary>
|
||||
private static int PreparePhiSilica()
|
||||
{
|
||||
try
|
||||
{
|
||||
PhiSilicaLafHelper.TryUnlock();
|
||||
var readyState = Microsoft.Windows.AI.Text.LanguageModel.GetReadyState();
|
||||
|
||||
Console.Error.WriteLine($"[phi-silica] LAF unlock status: {PhiSilicaLafHelper.LastUnlockStatus}; ReadyState: {readyState}");
|
||||
|
||||
if (readyState is Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem
|
||||
or Microsoft.Windows.AI.AIFeatureReadyState.DisabledByUser)
|
||||
{
|
||||
Console.Out.WriteLine("NotSupported");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (readyState == Microsoft.Windows.AI.AIFeatureReadyState.Ready)
|
||||
{
|
||||
Console.Out.WriteLine("Ready");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Run on a thread-pool (MTA) thread: the WinRT async operation does not
|
||||
// marshal correctly when blocked on from the [STAThread] entry point.
|
||||
var result = System.Threading.Tasks.Task.Run(
|
||||
() => Microsoft.Windows.AI.Text.LanguageModel.EnsureReadyAsync().AsTask()).GetAwaiter().GetResult();
|
||||
|
||||
if (result.Status != Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
|
||||
{
|
||||
int hresult = result.ExtendedError?.HResult ?? 0;
|
||||
Console.Error.WriteLine($"[phi-silica] EnsureReadyAsync Status: {result.Status}; HRESULT: 0x{hresult:X8}; Message: {result.ExtendedError?.Message}");
|
||||
Console.Error.WriteLine(result.ExtendedError?.Message ?? result.Status.ToString());
|
||||
Console.Out.WriteLine("Failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.Out.WriteLine("Ready");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
Console.Out.WriteLine("NotSupported");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,21 +33,14 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly SettingsUtils _settingsUtil;
|
||||
private readonly Func<string, string> _getLocalizedString;
|
||||
|
||||
private static string Version => Assembly.GetExecutingAssembly()?.GetName()?.Version?.ToString() ?? string.Empty;
|
||||
|
||||
public CustomActionKernelQueryCacheService(IUserSettings userSettings, IFileSystem fileSystem)
|
||||
: this(userSettings, fileSystem, ResourceLoaderInstance.ResourceLoader.GetString)
|
||||
{
|
||||
}
|
||||
|
||||
internal CustomActionKernelQueryCacheService(IUserSettings userSettings, IFileSystem fileSystem, Func<string, string> getLocalizedString)
|
||||
{
|
||||
_userSettings = userSettings;
|
||||
_fileSystem = fileSystem;
|
||||
_settingsUtil = new SettingsUtils(fileSystem);
|
||||
_getLocalizedString = getLocalizedString;
|
||||
|
||||
_userSettings.Changed += OnUserSettingsChanged;
|
||||
|
||||
@@ -119,7 +112,7 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
|
||||
let metadata = pair.Value
|
||||
where !string.IsNullOrEmpty(metadata.ResourceId)
|
||||
where metadata.IsCoreAction || _userSettings.AdditionalActions.Contains(format)
|
||||
select _getLocalizedString(metadata.ResourceId);
|
||||
select ResourceLoaderInstance.ResourceLoader.GetString(metadata.ResourceId);
|
||||
|
||||
var customActionPrompts = from customAction in _userSettings.CustomActions
|
||||
select customAction.Prompt;
|
||||
|
||||
@@ -40,15 +40,10 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
this.userSettings = userSettings;
|
||||
}
|
||||
|
||||
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress, string systemPromptOverride = null, string providerIdOverride = null)
|
||||
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var pasteConfig = userSettings?.PasteAIConfiguration;
|
||||
var providerConfig = BuildProviderConfig(pasteConfig, providerIdOverride);
|
||||
|
||||
if (systemPromptOverride != null)
|
||||
{
|
||||
providerConfig.SystemPrompt = systemPromptOverride;
|
||||
}
|
||||
var providerConfig = BuildProviderConfig(pasteConfig);
|
||||
|
||||
return await TransformAsync(prompt, inputText, imageBytes, providerConfig, cancellationToken, progress);
|
||||
}
|
||||
@@ -153,26 +148,13 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
|
||||
}
|
||||
|
||||
private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config, string providerIdOverride = null)
|
||||
private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config)
|
||||
{
|
||||
config ??= new PasteAIConfiguration();
|
||||
PasteAIProviderDefinition provider;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(providerIdOverride))
|
||||
{
|
||||
provider = config.Providers?.FirstOrDefault(p => string.Equals(p.Id, providerIdOverride, StringComparison.OrdinalIgnoreCase))
|
||||
?? config.ActiveProvider
|
||||
?? config.Providers?.FirstOrDefault()
|
||||
?? new PasteAIProviderDefinition();
|
||||
}
|
||||
else
|
||||
{
|
||||
provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition();
|
||||
}
|
||||
|
||||
var provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition();
|
||||
var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
|
||||
var systemPrompt = string.IsNullOrWhiteSpace(provider.SystemPrompt) ? DefaultSystemPrompt : provider.SystemPrompt;
|
||||
var apiKey = AcquireApiKey(serviceType, provider.Id);
|
||||
var apiKey = AcquireApiKey(serviceType);
|
||||
var modelName = provider.ModelName;
|
||||
|
||||
var providerConfig = new PasteAIConfig
|
||||
@@ -191,14 +173,15 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
return providerConfig;
|
||||
}
|
||||
|
||||
private string AcquireApiKey(AIServiceType serviceType, string providerId)
|
||||
private string AcquireApiKey(AIServiceType serviceType)
|
||||
{
|
||||
if (!RequiresApiKey(serviceType))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return credentialsProvider.GetKey(serviceType, providerId ?? string.Empty);
|
||||
credentialsProvider.Refresh();
|
||||
return credentialsProvider.GetKey() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool RequiresApiKey(AIServiceType serviceType)
|
||||
@@ -207,8 +190,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
AIServiceType.Onnx => false,
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.FoundryLocal => false,
|
||||
AIServiceType.PhiSilica => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface ICustomActionTransformService
|
||||
{
|
||||
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress, string systemPromptOverride = null, string providerIdOverride = null);
|
||||
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
SemanticKernelPasteProvider.Registration,
|
||||
LocalModelPasteProvider.Registration,
|
||||
FoundryLocalPasteProvider.Registration,
|
||||
PhiSilicaPasteProvider.Registration,
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>> ProviderFactories = CreateProviderFactories();
|
||||
|
||||
@@ -1,208 +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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.Windows.AI;
|
||||
using Microsoft.Windows.AI.ContentSafety;
|
||||
using Microsoft.Windows.AI.Text;
|
||||
using PhiSilicaLanguageModel = Microsoft.Windows.AI.Text.LanguageModel;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions;
|
||||
|
||||
public sealed class PhiSilicaPasteProvider : IPasteAIProvider
|
||||
{
|
||||
private static readonly IReadOnlyCollection<AIServiceType> SupportedTypes = new[]
|
||||
{
|
||||
AIServiceType.PhiSilica,
|
||||
};
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new PhiSilicaPasteProvider(config));
|
||||
|
||||
private static readonly SemaphoreSlim _initLock = new(1, 1);
|
||||
private static PhiSilicaLanguageModel _cachedModel;
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
|
||||
public PhiSilicaPasteProvider(PasteAIConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
PhiSilicaLafHelper.TryUnlock();
|
||||
var readyState = PhiSilicaLanguageModel.GetReadyState();
|
||||
return Task.FromResult(readyState is not (AIFeatureReadyState.NotSupportedOnCurrentSystem or AIFeatureReadyState.DisabledByUser));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
try
|
||||
{
|
||||
var systemPrompt = request.SystemPrompt;
|
||||
if (string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
throw new PasteActionException(
|
||||
"System prompt is required for Phi Silica",
|
||||
new ArgumentException("System prompt must be provided", nameof(request)));
|
||||
}
|
||||
|
||||
var prompt = request.Prompt;
|
||||
var inputText = request.InputText;
|
||||
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
throw new PasteActionException(
|
||||
"Prompt and input text are required",
|
||||
new ArgumentException("Prompt and input text must be provided", nameof(request)));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var languageModel = await GetOrCreateModelAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress?.Report(0.1);
|
||||
|
||||
var contentFilterOptions = new ContentFilterOptions();
|
||||
var context = languageModel.CreateContext(systemPrompt, contentFilterOptions);
|
||||
|
||||
var userPrompt = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Text:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
|
||||
if ((ulong)userPrompt.Length > languageModel.GetUsablePromptLength(context, userPrompt))
|
||||
{
|
||||
throw new PasteActionException(
|
||||
"Prompt is too large for the Phi Silica model context",
|
||||
new InvalidOperationException("Prompt exceeds usable prompt length"),
|
||||
aiServiceMessage: "The input text is too large for on-device processing. Try with shorter text.");
|
||||
}
|
||||
|
||||
var options = new LanguageModelOptions
|
||||
{
|
||||
ContentFilterOptions = contentFilterOptions,
|
||||
};
|
||||
|
||||
var result = await languageModel.GenerateResponseAsync(context, userPrompt, options).AsTask(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress?.Report(0.8);
|
||||
|
||||
if (result.Status != LanguageModelResponseStatus.Complete)
|
||||
{
|
||||
var statusMessage = result.Status switch
|
||||
{
|
||||
LanguageModelResponseStatus.BlockedByPolicy => "Response was blocked by policy.",
|
||||
LanguageModelResponseStatus.PromptBlockedByContentModeration => "Prompt was blocked by content moderation.",
|
||||
LanguageModelResponseStatus.ResponseBlockedByContentModeration => "Response was blocked by content moderation.",
|
||||
LanguageModelResponseStatus.PromptLargerThanContext => "Prompt is too large for the model context.",
|
||||
_ => $"Unexpected status: {result.Status}",
|
||||
};
|
||||
|
||||
throw new PasteActionException(
|
||||
$"Phi Silica returned status: {result.Status}",
|
||||
new InvalidOperationException($"LanguageModel response status: {result.Status}"),
|
||||
aiServiceMessage: statusMessage);
|
||||
}
|
||||
|
||||
var responseText = result.Text ?? string.Empty;
|
||||
request.Usage = AIServiceUsage.None;
|
||||
|
||||
progress?.Report(1.0);
|
||||
|
||||
return responseText;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (PasteActionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PasteActionException(
|
||||
"Failed to generate response using Phi Silica",
|
||||
ex,
|
||||
aiServiceMessage: $"Error details: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<PhiSilicaLanguageModel> GetOrCreateModelAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cachedModel is not null)
|
||||
{
|
||||
return _cachedModel;
|
||||
}
|
||||
|
||||
await _initLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_cachedModel is not null)
|
||||
{
|
||||
return _cachedModel;
|
||||
}
|
||||
|
||||
PhiSilicaLafHelper.TryUnlock();
|
||||
var readyState = PhiSilicaLanguageModel.GetReadyState();
|
||||
|
||||
if (readyState is AIFeatureReadyState.NotSupportedOnCurrentSystem or AIFeatureReadyState.DisabledByUser)
|
||||
{
|
||||
throw new PasteActionException(
|
||||
"Phi Silica is not supported on this device. A Copilot+ PC is required.",
|
||||
new InvalidOperationException("Phi Silica requires a Copilot+ PC with an NPU."),
|
||||
aiServiceMessage: "Phi Silica requires a Copilot+ PC with an NPU. For on-device AI on any Windows PC, consider using Foundry Local.");
|
||||
}
|
||||
|
||||
if (readyState is AIFeatureReadyState.NotReady)
|
||||
{
|
||||
var ensureResult = await PhiSilicaLanguageModel.EnsureReadyAsync().AsTask(cancellationToken).ConfigureAwait(false);
|
||||
if (ensureResult.Status != AIFeatureReadyResultState.Success)
|
||||
{
|
||||
throw new PasteActionException(
|
||||
"Failed to prepare Phi Silica model",
|
||||
ensureResult.ExtendedError,
|
||||
aiServiceMessage: $"Model preparation failed (status: {ensureResult.Status})");
|
||||
}
|
||||
}
|
||||
|
||||
if (PhiSilicaLanguageModel.GetReadyState() is not AIFeatureReadyState.Ready)
|
||||
{
|
||||
throw new PasteActionException(
|
||||
"Phi Silica model is not ready",
|
||||
new InvalidOperationException("Phi Silica model is not in Ready state after preparation."),
|
||||
aiServiceMessage: "Phi Silica model is not available. Please ensure the model is downloaded and ready.");
|
||||
}
|
||||
|
||||
_cachedModel = await PhiSilicaLanguageModel.CreateAsync().AsTask(cancellationToken).ConfigureAwait(false);
|
||||
return _cachedModel;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_initLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = null,
|
||||
ReasoningEffort = "minimal",
|
||||
},
|
||||
_ => new PromptExecutionSettings(),
|
||||
};
|
||||
|
||||
@@ -55,13 +55,6 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
|
||||
return !string.IsNullOrEmpty(GetKey());
|
||||
}
|
||||
|
||||
public string GetKey(AIServiceType serviceType, string providerId)
|
||||
{
|
||||
var normalizedType = NormalizeServiceType(serviceType);
|
||||
var entry = BuildCredentialEntry(normalizedType, providerId ?? string.Empty);
|
||||
return LoadKey(entry);
|
||||
}
|
||||
|
||||
public bool Refresh()
|
||||
{
|
||||
using (_syncRoot.EnterScope())
|
||||
@@ -128,7 +121,6 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
|
||||
try
|
||||
{
|
||||
var credential = new PasswordVault().Retrieve(entry.Value.Resource, entry.Value.Username);
|
||||
credential?.RetrievePassword();
|
||||
return credential?.Password ?? string.Empty;
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -168,7 +160,6 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
|
||||
case AIServiceType.ML:
|
||||
case AIServiceType.Onnx:
|
||||
case AIServiceType.Ollama:
|
||||
case AIServiceType.PhiSilica:
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,14 +21,6 @@ public interface IAICredentialsProvider
|
||||
/// <returns>Credential string or <see cref="string.Empty"/> when missing.</returns>
|
||||
string GetKey();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the credential for a specific AI provider.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">The AI service type.</param>
|
||||
/// <param name="providerId">The provider identifier.</param>
|
||||
/// <returns>Credential string or <see cref="string.Empty"/> when missing.</returns>
|
||||
string GetKey(AIServiceType serviceType, string providerId);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the cached credential for the active AI provider.
|
||||
/// </summary>
|
||||
|
||||
@@ -12,5 +12,5 @@ namespace AdvancedPaste.Services;
|
||||
|
||||
public interface IKernelService
|
||||
{
|
||||
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress, string providerIdOverride = null);
|
||||
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public abstract class KernelServiceBase(
|
||||
|
||||
protected abstract IKernelRuntimeConfiguration GetRuntimeConfiguration();
|
||||
|
||||
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress, string providerIdOverride = null)
|
||||
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
|
||||
@@ -9,18 +9,15 @@ using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Settings;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService, IUserSettings userSettings) : IPasteFormatExecutor
|
||||
public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService) : IPasteFormatExecutor
|
||||
{
|
||||
private readonly IKernelService _kernelService = kernelService;
|
||||
private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
|
||||
private readonly IUserSettings _userSettings = userSettings;
|
||||
|
||||
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
@@ -39,9 +36,8 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
|
||||
return await Task.Run(async () =>
|
||||
pasteFormat.Format switch
|
||||
{
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress, pasteFormat.ProviderId),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress, providerIdOverride: pasteFormat.ProviderId))?.Content ?? string.Empty),
|
||||
PasteFormats.FixSpellingAndGrammar => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(GetFixSpellingPrompt(), await clipboardData.GetTextOrHtmlTextAsync(), null, cancellationToken, progress, GetFixSpellingSystemPrompt(), pasteFormat.ProviderId))?.Content ?? string.Empty),
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||
});
|
||||
}
|
||||
@@ -66,16 +62,4 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
|
||||
throw new ArgumentOutOfRangeException(nameof(format));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFixSpellingPrompt()
|
||||
{
|
||||
var customPrompt = _userSettings.FixSpellingAndGrammarPrompt;
|
||||
return string.IsNullOrWhiteSpace(customPrompt) ? AdvancedPasteDefaultPrompts.FixSpellingAndGrammar : customPrompt;
|
||||
}
|
||||
|
||||
private string GetFixSpellingSystemPrompt()
|
||||
{
|
||||
var customSystemPrompt = _userSettings.FixSpellingAndGrammarSystemPrompt;
|
||||
return string.IsNullOrWhiteSpace(customSystemPrompt) ? AdvancedPasteDefaultPrompts.FixSpellingAndGrammarSystem : customSystemPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,12 +232,6 @@
|
||||
<data name="PasteAsPlainText" xml:space="preserve">
|
||||
<value>Paste as plain text</value>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar" xml:space="preserve">
|
||||
<value>Fix spelling and grammar</value>
|
||||
</data>
|
||||
<data name="CoachingExplanationTitle.Text" xml:space="preserve">
|
||||
<value>What was changed and why</value>
|
||||
</data>
|
||||
<data name="ImageToText" xml:space="preserve">
|
||||
<value>Image to text</value>
|
||||
</data>
|
||||
|
||||
@@ -16,8 +16,8 @@ using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Settings;
|
||||
using Common.UI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
@@ -41,7 +41,6 @@ namespace AdvancedPaste.ViewModels
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
||||
private readonly IAICredentialsProvider _credentialsProvider;
|
||||
private readonly ICustomActionTransformService _customActionTransformService;
|
||||
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
@@ -259,12 +258,11 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public event EventHandler PreviewRequested;
|
||||
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor, ICustomActionTransformService customActionTransformService)
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
{
|
||||
_credentialsProvider = credentialsProvider;
|
||||
_userSettings = userSettings;
|
||||
_pasteFormatExecutor = pasteFormatExecutor;
|
||||
_customActionTransformService = customActionTransformService;
|
||||
|
||||
GeneratedResponses = [];
|
||||
GeneratedResponses.CollectionChanged += (s, e) =>
|
||||
@@ -343,21 +341,11 @@ namespace AdvancedPaste.ViewModels
|
||||
});
|
||||
}
|
||||
|
||||
private PasteFormat CreateStandardPasteFormat(PasteFormats format)
|
||||
{
|
||||
var providerId = GetProviderIdForFormat(format);
|
||||
return PasteFormat.CreateStandardFormat(format, AvailableClipboardFormats, IsCustomAIServiceEnabled, ResourceLoaderInstance.ResourceLoader.GetString, providerId);
|
||||
}
|
||||
private PasteFormat CreateStandardPasteFormat(PasteFormats format) =>
|
||||
PasteFormat.CreateStandardFormat(format, AvailableClipboardFormats, IsCustomAIServiceEnabled, ResourceLoaderInstance.ResourceLoader.GetString);
|
||||
|
||||
private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery, string providerId = null) =>
|
||||
PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled, providerId);
|
||||
|
||||
private string GetProviderIdForFormat(PasteFormats format) =>
|
||||
format switch
|
||||
{
|
||||
PasteFormats.FixSpellingAndGrammar => _userSettings.FixSpellingAndGrammarProviderId,
|
||||
_ => string.Empty,
|
||||
};
|
||||
private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery) =>
|
||||
PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled);
|
||||
|
||||
private void UpdateAIProviderActiveFlags()
|
||||
{
|
||||
@@ -430,7 +418,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
UpdateFormats(
|
||||
CustomActionPasteFormats,
|
||||
IsCustomAIServiceEnabled ? _userSettings.CustomActions.Select(customAction => CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true, customAction.ProviderId)) : []);
|
||||
IsCustomAIServiceEnabled ? _userSettings.CustomActions.Select(customAction => CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true)) : []);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -551,7 +539,6 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
PasteActionError = PasteActionError.None;
|
||||
Query = string.Empty;
|
||||
CoachingExplanation = null;
|
||||
|
||||
await ReadClipboardAsync();
|
||||
|
||||
@@ -629,12 +616,6 @@ namespace AdvancedPaste.ViewModels
|
||||
[ObservableProperty]
|
||||
private string _customFormatResult;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasCoachingExplanation))]
|
||||
private string _coachingExplanation;
|
||||
|
||||
public bool HasCoachingExplanation => !string.IsNullOrEmpty(CoachingExplanation);
|
||||
|
||||
[RelayCommand]
|
||||
public async Task PasteCustomAsync()
|
||||
{
|
||||
@@ -680,37 +661,17 @@ namespace AdvancedPaste.ViewModels
|
||||
[RelayCommand]
|
||||
public void OpenSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var exePath = System.IO.Path.Combine(
|
||||
ManagedCommon.PowerToysPathResolver.GetPowerToysInstallPath(),
|
||||
"PowerToys.exe");
|
||||
|
||||
if (exePath != null && System.IO.File.Exists(exePath))
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
Arguments = "--open-settings=AdvancedPaste",
|
||||
UseShellExecute = false,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to open settings", ex);
|
||||
}
|
||||
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste);
|
||||
GetMainWindow()?.Close();
|
||||
}
|
||||
|
||||
internal async Task ExecutePasteFormatAsync(PasteFormats format, PasteActionSource source, bool forceCoaching = false)
|
||||
internal async Task ExecutePasteFormatAsync(PasteFormats format, PasteActionSource source)
|
||||
{
|
||||
await ReadClipboardAsync();
|
||||
await ExecutePasteFormatAsync(CreateStandardPasteFormat(format), source, forceCoaching);
|
||||
await ExecutePasteFormatAsync(CreateStandardPasteFormat(format), source);
|
||||
}
|
||||
|
||||
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, bool forceCoaching = false)
|
||||
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
||||
{
|
||||
if (IsBusy)
|
||||
{
|
||||
@@ -743,30 +704,12 @@ namespace AdvancedPaste.ViewModels
|
||||
await delayTask;
|
||||
|
||||
var outputText = await dataPackage.GetView().GetTextOrEmptyAsync();
|
||||
bool isCoachingAction = pasteFormat.Format == PasteFormats.FixSpellingAndGrammar &&
|
||||
(forceCoaching || (_userSettings.FixSpellingAndGrammarCoachingEnabled && !_userSettings.FixSpellingAndGrammarCoachingShortcutSet));
|
||||
bool shouldPreview = pasteFormat.Metadata.CanPreview && _userSettings.ShowCustomPreview && !string.IsNullOrEmpty(outputText) && source != PasteActionSource.GlobalKeyboardShortcut;
|
||||
|
||||
// Coaching mode forces preview even for global keyboard shortcuts
|
||||
if (isCoachingAction && !string.IsNullOrEmpty(outputText))
|
||||
{
|
||||
shouldPreview = true;
|
||||
}
|
||||
|
||||
if (shouldPreview)
|
||||
{
|
||||
GeneratedResponses.Add(outputText);
|
||||
CurrentResponseIndex = GeneratedResponses.Count - 1;
|
||||
|
||||
if (isCoachingAction)
|
||||
{
|
||||
await GenerateCoachingExplanationAsync(outputText);
|
||||
}
|
||||
else
|
||||
{
|
||||
CoachingExplanation = null;
|
||||
}
|
||||
|
||||
PreviewRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
else
|
||||
@@ -787,65 +730,6 @@ namespace AdvancedPaste.ViewModels
|
||||
Logger.LogDebug($"Finished executing {pasteFormat.Format} from source {source}; timeTakenMs={elapsedWatch.ElapsedMilliseconds}");
|
||||
}
|
||||
|
||||
private async Task GenerateCoachingExplanationAsync(string correctedText)
|
||||
{
|
||||
try
|
||||
{
|
||||
var originalText = ClipboardData != null ? await ClipboardData.GetTextOrEmptyAsync() : string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(originalText))
|
||||
{
|
||||
CoachingExplanation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
static string NormalizeForComparison(string s) =>
|
||||
s.Replace('\u2018', '\'') // left single quote
|
||||
.Replace('\u2019', '\'') // right single quote / apostrophe
|
||||
.Replace('\u201C', '"') // left double quote
|
||||
.Replace('\u201D', '"') // right double quote
|
||||
.Replace('\u2013', '-') // en dash
|
||||
.Replace('\u2014', '-'); // em dash
|
||||
|
||||
if (string.Equals(NormalizeForComparison(originalText), NormalizeForComparison(correctedText), StringComparison.Ordinal))
|
||||
{
|
||||
CoachingExplanation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var coachingInstruction = string.IsNullOrWhiteSpace(_userSettings.FixSpellingAndGrammarCoachingPrompt)
|
||||
? AdvancedPasteDefaultPrompts.FixSpellingAndGrammarCoaching
|
||||
: _userSettings.FixSpellingAndGrammarCoachingPrompt;
|
||||
var coachingInputText = $"Original:\n\"{originalText}\"\n\nCorrected:\n\"{correctedText}\"";
|
||||
|
||||
var coachingSystemPrompt = string.IsNullOrWhiteSpace(_userSettings.FixSpellingAndGrammarCoachingSystemPrompt)
|
||||
? AdvancedPasteDefaultPrompts.FixSpellingAndGrammarCoachingSystem
|
||||
: _userSettings.FixSpellingAndGrammarCoachingSystemPrompt;
|
||||
|
||||
var coachingProviderId = _userSettings.FixSpellingAndGrammarCoachingProviderId;
|
||||
if (string.IsNullOrWhiteSpace(coachingProviderId))
|
||||
{
|
||||
coachingProviderId = _userSettings.FixSpellingAndGrammarProviderId;
|
||||
}
|
||||
|
||||
var result = await _customActionTransformService.TransformAsync(
|
||||
coachingInstruction,
|
||||
coachingInputText,
|
||||
null,
|
||||
_pasteActionCancellationTokenSource?.Token ?? CancellationToken.None,
|
||||
null,
|
||||
coachingSystemPrompt,
|
||||
string.IsNullOrWhiteSpace(coachingProviderId) ? null : coachingProviderId);
|
||||
|
||||
CoachingExplanation = result?.Content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error generating coaching explanation", ex);
|
||||
CoachingExplanation = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task ExecutePasteFormatAsync(VirtualKey key)
|
||||
{
|
||||
var pasteFormat = StandardPasteFormats.Concat(CustomActionPasteFormats)
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
#include <windows.h>
|
||||
#include "resource.h"
|
||||
#include "../../../../common/version/version.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
#include "winres.h"
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||
END
|
||||
END
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
|
||||
@@ -100,30 +100,25 @@ HRESULT AdvancedPasteProcessManager::start_process(const std::wstring& pipe_name
|
||||
{
|
||||
const unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
const auto launch_direct_exe = [&]() -> HRESULT {
|
||||
// Fallback: launch exe directly (dev builds without GenerateAppxPackageOnBuild)
|
||||
const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_name);
|
||||
const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_name);
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
|
||||
sei.lpFile = L"WinUI3Apps\\PowerToys.AdvancedPaste.exe";
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = executable_args.data();
|
||||
if (ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::trace("Successfully started Advanced Paste process (direct)");
|
||||
terminate_process();
|
||||
m_hProcess = sei.hProcess;
|
||||
return S_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Advanced Paste process failed to start. {}", get_last_error_or_default(GetLastError()));
|
||||
return E_FAIL;
|
||||
}
|
||||
};
|
||||
|
||||
return launch_direct_exe();
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
|
||||
sei.lpFile = L"WinUI3Apps\\PowerToys.AdvancedPaste.exe";
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = executable_args.data();
|
||||
if (ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::trace("Successfully started Advanced Paste process");
|
||||
terminate_process();
|
||||
m_hProcess = sei.hProcess;
|
||||
return S_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Advanced Paste process failed to start. {}", get_last_error_or_default(GetLastError()));
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT AdvancedPasteProcessManager::start_named_pipe_server(const std::wstring& pipe_name)
|
||||
@@ -180,9 +175,8 @@ HRESULT AdvancedPasteProcessManager::start_named_pipe_server(const std::wstring&
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for client. AdvancedPaste under sparse identity can take >5s on cold start to
|
||||
// bootstrap WinAppSDK + DI host before connecting back to this pipe.
|
||||
const constexpr DWORD client_timeout_millis = 15000;
|
||||
// Wait for client.
|
||||
const constexpr DWORD client_timeout_millis = 5000;
|
||||
switch (WaitForSingleObject(overlapped.hEvent, client_timeout_millis))
|
||||
{
|
||||
case WAIT_OBJECT_0:
|
||||
|
||||
@@ -66,8 +66,6 @@ namespace
|
||||
const wchar_t JSON_KEY_PROVIDERS[] = L"providers";
|
||||
const wchar_t JSON_KEY_SERVICE_TYPE[] = L"service-type";
|
||||
const wchar_t JSON_KEY_ENABLE_ADVANCED_AI[] = L"enable-advanced-ai";
|
||||
const wchar_t JSON_KEY_COACHING_SHORTCUT[] = L"coaching-shortcut";
|
||||
const wchar_t JSON_KEY_COACHING_ENABLED[] = L"coaching-enabled";
|
||||
const wchar_t JSON_KEY_VALUE[] = L"value";
|
||||
}
|
||||
|
||||
@@ -257,21 +255,6 @@ private:
|
||||
};
|
||||
|
||||
m_additional_actions.push_back(additionalAction);
|
||||
|
||||
// Register coaching shortcut as a separate hotkey with a "-coaching" suffix ID
|
||||
if (action.HasKey(JSON_KEY_COACHING_SHORTCUT) && action.GetNamedBoolean(JSON_KEY_COACHING_ENABLED, false))
|
||||
{
|
||||
auto coachingHotkey = parse_single_hotkey(action.GetNamedObject(JSON_KEY_COACHING_SHORTCUT), actionIsShown);
|
||||
if (coachingHotkey.key != 0)
|
||||
{
|
||||
const AdditionalAction coachingAction
|
||||
{
|
||||
std::wstring(actionName.c_str()) + L"-coaching",
|
||||
coachingHotkey
|
||||
};
|
||||
m_additional_actions.push_back(coachingAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -424,7 +407,6 @@ private:
|
||||
// Define the expected order to ensure consistent hotkey ID assignment
|
||||
const std::vector<winrt::hstring> expectedOrder = {
|
||||
L"image-to-text",
|
||||
L"fix-spelling-and-grammar",
|
||||
L"paste-as-file",
|
||||
L"transcode"
|
||||
};
|
||||
@@ -1000,12 +982,6 @@ public:
|
||||
m_triggerEventWaiter.start(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT, [this](DWORD) {
|
||||
// Same logic as hotkeyId == 1 (m_advanced_paste_ui_hotkey)
|
||||
Logger::trace(L"AdvancedPaste ShowUI event triggered");
|
||||
|
||||
if (m_auto_copy_selection_custom_action)
|
||||
{
|
||||
send_copy_selection(); // best-effort; ignore failure
|
||||
}
|
||||
|
||||
m_process_manager.start();
|
||||
m_process_manager.bring_to_front();
|
||||
m_process_manager.send_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE);
|
||||
@@ -1056,11 +1032,13 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Try to capture selected text for all hotkey actions when the setting is enabled.
|
||||
// If nothing is selected (clipboard unchanged), fall through to use existing clipboard content.
|
||||
if (m_auto_copy_selection_custom_action)
|
||||
if (is_custom_action_hotkey && m_auto_copy_selection_custom_action)
|
||||
{
|
||||
send_copy_selection(); // best-effort; ignore failure
|
||||
if (!send_copy_selection())
|
||||
{
|
||||
Logger::warn(L"Auto-copy: failed to copy selection for custom action index {} — aborting action", custom_action_index);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_process_manager.start();
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!-- This file is read by XES, which we use in our Release builds. -->
|
||||
<PropertyGroup Label="Version">
|
||||
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
||||
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
||||
<VersionMajor>0</VersionMajor>
|
||||
<VersionMinor>9</VersionMinor>
|
||||
<VersionInfoProductName>PowerToys Advanced Paste</VersionInfoProductName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1149,10 +1149,13 @@ VideoRecordingSession::VideoRecordingSession(
|
||||
// Store frame interval for timeout-based frame production when webcam is active.
|
||||
m_frameIntervalTicks = ( frameRate > 0 ) ? ( 10'000'000LL / frameRate ) : 333'333LL;
|
||||
|
||||
// NOTE: Audio encoding profile (m_encodingProfile.Audio) is set in
|
||||
// StartAsync() after the audio graph is fully initialized, not here.
|
||||
// Calling GetEncodingProperties() before InitializeAsync completes
|
||||
// would crash because m_audioOutputNode is still null.
|
||||
if (m_audioGenerator)
|
||||
{
|
||||
// Always set up audio profile for loopback capture (stereo AAC)
|
||||
auto audioProps = m_audioGenerator->GetEncodingProperties();
|
||||
m_encodingProfile.Audio(winrt::AudioEncodingProperties::CreateAac(
|
||||
audioProps.SampleRate(), audioProps.ChannelCount(), 192000));
|
||||
}
|
||||
|
||||
// Describe our input: uncompressed BGRA8 buffers
|
||||
auto properties = winrt::VideoEncodingProperties::CreateUncompressed(
|
||||
@@ -1230,16 +1233,7 @@ winrt::IAsyncAction VideoRecordingSession::StartAsync()
|
||||
co_await m_audioGenerator->InitializeAsync();
|
||||
}
|
||||
RecDiag( L"StartAsync: audio initialized\n" );
|
||||
|
||||
// Set up the audio encoding profile now that the audio graph is
|
||||
// fully initialized. GetEncodingProperties() requires
|
||||
// m_audioOutputNode to be valid, which is only guaranteed after
|
||||
// InitializeAsync completes.
|
||||
auto audioProps = m_audioGenerator->GetEncodingProperties();
|
||||
m_encodingProfile.Audio(winrt::AudioEncodingProperties::CreateAac(
|
||||
audioProps.SampleRate(), audioProps.ChannelCount(), 192000));
|
||||
|
||||
m_streamSource = winrt::MediaStreamSource(m_videoDescriptor, winrt::AudioStreamDescriptor(audioProps));
|
||||
m_streamSource = winrt::MediaStreamSource(m_videoDescriptor, winrt::AudioStreamDescriptor(m_audioGenerator->GetEncodingProperties()));
|
||||
}
|
||||
else {
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.20250829.1" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
|
||||
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <keyboardmanager/KeyboardManagerEditor/KeyboardManagerEditor.h>
|
||||
#include <keyboardmanager/KeyboardManagerEditorLibrary/EditorHelpers.h>
|
||||
@@ -41,6 +43,35 @@ extern "C"
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Populates the template metadata fields on a marshaled mapping. Always sets both pointers
|
||||
// (the editor frees every field), using empty strings for non-template / non-RunProgram entries.
|
||||
// Note: kept void-returning so it is valid inside the surrounding extern "C" block (a C-linkage
|
||||
// function may not return a C++ type such as std::wstring).
|
||||
void SetTemplateMetadata(ShortcutMapping* mapping, const Shortcut& targetShortcut)
|
||||
{
|
||||
mapping->templateId = AllocateAndCopyString(targetShortcut.templateId);
|
||||
|
||||
if (targetShortcut.templateParameters.empty())
|
||||
{
|
||||
mapping->templateParametersJson = AllocateAndCopyString(L"");
|
||||
return;
|
||||
}
|
||||
|
||||
json::JsonObject paramsObj;
|
||||
for (auto const& [k, v] : targetShortcut.templateParameters)
|
||||
{
|
||||
paramsObj.SetNamedValue(k, json::JsonValue::CreateStringValue(v));
|
||||
}
|
||||
|
||||
mapping->templateParametersJson = AllocateAndCopyString(paramsObj.Stringify().c_str());
|
||||
}
|
||||
|
||||
void SetEmptyTemplateMetadata(ShortcutMapping* mapping)
|
||||
{
|
||||
mapping->templateId = AllocateAndCopyString(L"");
|
||||
mapping->templateParametersJson = AllocateAndCopyString(L"");
|
||||
}
|
||||
|
||||
int GetSingleKeyRemapCount(void* config)
|
||||
{
|
||||
auto mapping = static_cast<MappingConfiguration*>(config);
|
||||
@@ -382,6 +413,17 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
mapping->uriToOpen = AllocateAndCopyString(L"");
|
||||
}
|
||||
|
||||
// Carry template metadata back to the editor so a template mapping re-opens as RunTemplate.
|
||||
// Only RunProgram shortcuts ever carry it; every other entry gets empty (but allocated) fields.
|
||||
if (targetShortcutUnion.index() == 1)
|
||||
{
|
||||
SetTemplateMetadata(mapping, std::get<Shortcut>(targetShortcutUnion));
|
||||
}
|
||||
else
|
||||
{
|
||||
SetEmptyTemplateMetadata(mapping);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -483,6 +525,17 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
mapping->uriToOpen = AllocateAndCopyString(L"");
|
||||
}
|
||||
|
||||
// Carry template metadata back to the editor so a template mapping re-opens as RunTemplate.
|
||||
// Only RunProgram shortcuts ever carry it; every other entry gets empty (but allocated) fields.
|
||||
if (targetShortcutUnion.index() == 1)
|
||||
{
|
||||
SetTemplateMetadata(mapping, std::get<Shortcut>(targetShortcutUnion));
|
||||
}
|
||||
else
|
||||
{
|
||||
SetEmptyTemplateMetadata(mapping);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -527,13 +580,15 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
const wchar_t* originalKeys,
|
||||
const wchar_t* targetKeys,
|
||||
const wchar_t* targetApp,
|
||||
int operationType,
|
||||
int operationType,
|
||||
const wchar_t* appPathOrUri,
|
||||
const wchar_t* args,
|
||||
const wchar_t* startDirectory,
|
||||
int elevation,
|
||||
int ifRunningAction,
|
||||
int visibility)
|
||||
int visibility,
|
||||
const wchar_t* templateId,
|
||||
const wchar_t* templateParametersJson)
|
||||
{
|
||||
auto mappingConfig = static_cast<MappingConfiguration*>(config);
|
||||
|
||||
@@ -558,6 +613,28 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
|
||||
std::get<Shortcut>(targetShortcut).alreadyRunningAction = static_cast<Shortcut::ProgramAlreadyRunningAction>(ifRunningAction);
|
||||
std::get<Shortcut>(targetShortcut).startWindowType = static_cast<Shortcut::StartWindowType>(visibility);
|
||||
std::get<Shortcut>(targetShortcut).operationType = static_cast<Shortcut::OperationType>(operationType);
|
||||
|
||||
// Optional template metadata — only set when provided.
|
||||
if (templateId && templateId[0] != L'\0')
|
||||
{
|
||||
std::get<Shortcut>(targetShortcut).templateId = std::wstring(templateId);
|
||||
}
|
||||
if (templateParametersJson && templateParametersJson[0] != L'\0')
|
||||
{
|
||||
json::JsonObject paramsObj;
|
||||
if (json::JsonObject::TryParse(templateParametersJson, paramsObj))
|
||||
{
|
||||
for (auto const& kv : paramsObj)
|
||||
{
|
||||
if (kv.Value().ValueType() == json::JsonValueType::String)
|
||||
{
|
||||
std::get<Shortcut>(targetShortcut).templateParameters.emplace(
|
||||
std::wstring(kv.Key()),
|
||||
std::wstring(kv.Value().GetString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
targetShortcut = Shortcut(targetKeys);
|
||||
|
||||
@@ -33,6 +33,8 @@ struct ShortcutMapping
|
||||
wchar_t* programPath;
|
||||
wchar_t* programArgs;
|
||||
wchar_t* uriToOpen;
|
||||
wchar_t* templateId;
|
||||
wchar_t* templateParametersJson;
|
||||
};
|
||||
|
||||
extern "C"
|
||||
@@ -69,7 +71,9 @@ extern "C"
|
||||
const wchar_t* startDirectory = nullptr,
|
||||
int elevation = 0,
|
||||
int ifRunningAction = 0,
|
||||
int visibility = 0);
|
||||
int visibility = 0,
|
||||
const wchar_t* templateId = nullptr,
|
||||
const wchar_t* templateParametersJson = nullptr);
|
||||
|
||||
__declspec(dllexport) void GetKeyDisplayName(int keyCode, wchar_t* keyName, int maxCount);
|
||||
__declspec(dllexport) int GetKeyCodeFromName(const wchar_t* keyName);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
using KeyboardManagerEditorUI.Templates;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace KeyboardManagerEditorUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that the catalog data model deserializes the shipped powertoyscli.json schema
|
||||
/// shape correctly. (Reflection-based deserialization mirrors the source-generated context.)
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class CommandTemplateCatalogModelTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
private const string SeedCatalog = @"
|
||||
{
|
||||
""schemaVersion"": 1,
|
||||
""modules"": [
|
||||
{
|
||||
""id"": ""settings"",
|
||||
""displayResourceKey"": ""TemplateModule_Settings"",
|
||||
""iconGlyph"": """",
|
||||
""commands"": [
|
||||
{
|
||||
""id"": ""settings.openMain"",
|
||||
""displayResourceKey"": ""TemplateCmd_Settings_OpenMain"",
|
||||
""executable"": ""%LOCALAPPDATA%\\PowerToys\\PowerToys.exe"",
|
||||
""argsTemplate"": ""--open-settings"",
|
||||
""parameters"": []
|
||||
},
|
||||
{
|
||||
""id"": ""settings.openModule"",
|
||||
""displayResourceKey"": ""TemplateCmd_Settings_OpenModule"",
|
||||
""executable"": ""%LOCALAPPDATA%\\PowerToys\\PowerToys.exe"",
|
||||
""argsTemplate"": ""--open-settings={module}"",
|
||||
""parameters"": [
|
||||
{
|
||||
""name"": ""module"",
|
||||
""labelResourceKey"": ""TemplateParam_Module"",
|
||||
""type"": ""Combo"",
|
||||
""required"": true,
|
||||
""choices"": [
|
||||
{ ""value"": ""ColorPicker"", ""displayResourceKey"": ""Module_ColorPicker"" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_SeedCatalog_MapsAllFields()
|
||||
{
|
||||
var catalog = JsonSerializer.Deserialize<PowerToysCliCatalog>(SeedCatalog, Options);
|
||||
|
||||
Assert.IsNotNull(catalog);
|
||||
Assert.AreEqual(1, catalog!.SchemaVersion);
|
||||
Assert.AreEqual(1, catalog.Modules.Count);
|
||||
|
||||
var module = catalog.Modules[0];
|
||||
Assert.AreEqual("settings", module.Id);
|
||||
Assert.AreEqual("TemplateModule_Settings", module.DisplayResourceKey);
|
||||
Assert.AreEqual(2, module.Commands.Count);
|
||||
|
||||
var openMain = module.Commands.Single(c => c.Id == "settings.openMain");
|
||||
Assert.AreEqual("--open-settings", openMain.ArgsTemplate);
|
||||
Assert.AreEqual(0, openMain.Parameters.Count);
|
||||
|
||||
var openModule = module.Commands.Single(c => c.Id == "settings.openModule");
|
||||
Assert.AreEqual("--open-settings={module}", openModule.ArgsTemplate);
|
||||
Assert.AreEqual(1, openModule.Parameters.Count);
|
||||
|
||||
var param = openModule.Parameters[0];
|
||||
Assert.AreEqual("module", param.Name);
|
||||
Assert.AreEqual("Combo", param.Type);
|
||||
Assert.IsTrue(param.Required);
|
||||
Assert.IsNotNull(param.Choices);
|
||||
Assert.AreEqual("ColorPicker", param.Choices![0].Value);
|
||||
Assert.AreEqual("Module_ColorPicker", param.Choices[0].DisplayResourceKey);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_ForwardCompatibleVersion_StillParses()
|
||||
{
|
||||
// A newer, additive schemaVersion with an unknown field must deserialize (unknown fields ignored).
|
||||
const string newer = @"
|
||||
{
|
||||
""schemaVersion"": 2,
|
||||
""futureField"": ""ignored"",
|
||||
""modules"": [
|
||||
{ ""id"": ""m"", ""displayResourceKey"": ""k"", ""commands"": [] }
|
||||
]
|
||||
}";
|
||||
|
||||
var catalog = JsonSerializer.Deserialize<PowerToysCliCatalog>(newer, Options);
|
||||
|
||||
Assert.IsNotNull(catalog);
|
||||
Assert.AreEqual(2, catalog!.SchemaVersion);
|
||||
Assert.AreEqual(1, catalog.Modules.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Deserialize_OptionalChoices_DefaultToNull()
|
||||
{
|
||||
var param = JsonSerializer.Deserialize<TemplateParameter>(
|
||||
@"{ ""name"": ""p"", ""type"": ""Text"" }", Options);
|
||||
|
||||
Assert.IsNotNull(param);
|
||||
Assert.AreEqual("p", param!.Name);
|
||||
Assert.IsNull(param.Choices);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier Condition="'$(Platform)' == 'x64'">win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier Condition="'$(Platform)' == 'ARM64'">win-arm64</RuntimeIdentifier>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputPath>$(RepoRoot)$(Configuration)\$(Platform)\tests\KeyboardManagerEditorUITests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Source-link the pure (WinUI-free) template logic so it can be unit-tested without referencing
|
||||
the KeyboardManagerEditorUI WinExe app. These files have no UI/WinRT dependencies.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<Compile Include="..\KeyboardManagerEditorUI\Templates\TemplateResolver.cs" Link="Templates\TemplateResolver.cs" />
|
||||
<Compile Include="..\KeyboardManagerEditorUI\Templates\CommandTemplate.cs" Link="Templates\CommandTemplate.cs" />
|
||||
<Compile Include="..\KeyboardManagerEditorUI\Templates\CommandTemplateModule.cs" Link="Templates\CommandTemplateModule.cs" />
|
||||
<Compile Include="..\KeyboardManagerEditorUI\Templates\PowerToysCliCatalog.cs" Link="Templates\PowerToysCliCatalog.cs" />
|
||||
<Compile Include="..\KeyboardManagerEditorUI\Templates\TemplateParameter.cs" Link="Templates\TemplateParameter.cs" />
|
||||
<Compile Include="..\KeyboardManagerEditorUI\Templates\TemplateChoice.cs" Link="Templates\TemplateChoice.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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 KeyboardManagerEditorUI.Templates;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace KeyboardManagerEditorUI.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class TemplateResolverTests
|
||||
{
|
||||
private static CommandTemplate Template(string args, params TemplateParameter[] parameters)
|
||||
=> new()
|
||||
{
|
||||
Id = "test.cmd",
|
||||
Executable = "%LOCALAPPDATA%\\PowerToys\\PowerToys.exe",
|
||||
ArgsTemplate = args,
|
||||
Parameters = new List<TemplateParameter>(parameters),
|
||||
};
|
||||
|
||||
private static TemplateParameter Param(string name, string type = "Text", bool required = true)
|
||||
=> new() { Name = name, Type = type, Required = required };
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_SubstitutesPresentValue()
|
||||
{
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("--open-settings={module}", Param("module")),
|
||||
new Dictionary<string, string> { ["module"] = "ColorPicker" });
|
||||
|
||||
Assert.AreEqual("%LOCALAPPDATA%\\PowerToys\\PowerToys.exe", result.Executable);
|
||||
Assert.AreEqual("--open-settings=ColorPicker", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_MissingValue_SubstitutesEmpty()
|
||||
{
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("--open-settings={module}", Param("module")),
|
||||
new Dictionary<string, string>());
|
||||
|
||||
Assert.AreEqual("--open-settings=", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_NullValues_SubstitutesEmpty()
|
||||
{
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("--open-settings={module}", Param("module")),
|
||||
values: null);
|
||||
|
||||
Assert.AreEqual("--open-settings=", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_NoParameters_ReturnsTemplateVerbatim()
|
||||
{
|
||||
var result = TemplateResolver.Resolve(Template("--open-settings"), null);
|
||||
Assert.AreEqual("--open-settings", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_UnknownPlaceholder_LeftUntouched()
|
||||
{
|
||||
// {other} is not a declared parameter, so it must be preserved literally.
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("--a={module} --b={other}", Param("module")),
|
||||
new Dictionary<string, string> { ["module"] = "X" });
|
||||
|
||||
Assert.AreEqual("--a=X --b={other}", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_IsSinglePass_DoesNotReSubstituteInjectedPlaceholder()
|
||||
{
|
||||
// If the first parameter's value contains "{second}", the second pass must NOT replace it.
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("{first}-{second}", Param("first"), Param("second")),
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["first"] = "{second}",
|
||||
["second"] = "INJECTED",
|
||||
});
|
||||
|
||||
Assert.AreEqual("{second}-INJECTED", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_NoSubstringCollisionBetweenSimilarNames()
|
||||
{
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("{module}|{moduleVersion}", Param("module"), Param("moduleVersion")),
|
||||
new Dictionary<string, string> { ["module"] = "A", ["moduleVersion"] = "B" });
|
||||
|
||||
Assert.AreEqual("A|B", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_ValueWithSpace_IsQuoted()
|
||||
{
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("--path={p}", Param("p")),
|
||||
new Dictionary<string, string> { ["p"] = "C:\\Program Files\\App" });
|
||||
|
||||
Assert.AreEqual("--path=\"C:\\Program Files\\App\"", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_SimpleValue_NotQuoted()
|
||||
{
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("--x={p}", Param("p")),
|
||||
new Dictionary<string, string> { ["p"] = "Plain" });
|
||||
|
||||
Assert.AreEqual("--x=Plain", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resolve_ValueWithQuote_IsEscaped()
|
||||
{
|
||||
var result = TemplateResolver.Resolve(
|
||||
Template("--x={p}", Param("p")),
|
||||
new Dictionary<string, string> { ["p"] = "a\"b c" });
|
||||
|
||||
// Embedded quote is backslash-escaped and the whole value wrapped in quotes.
|
||||
Assert.AreEqual("--x=\"a\\\"b c\"", result.Args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void QuoteArgumentIfNeeded_TrailingBackslashesBeforeClosingQuote_AreDoubled()
|
||||
{
|
||||
// "a b\\" must become "a b\\\\" so the backslashes don't escape the closing quote.
|
||||
Assert.AreEqual("\"a b\\\\\"", TemplateResolver.QuoteArgumentIfNeeded("a b\\"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void QuoteArgumentIfNeeded_Empty_ReturnsEmpty()
|
||||
{
|
||||
Assert.AreEqual(string.Empty, TemplateResolver.QuoteArgumentIfNeeded(string.Empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="KeyboardManagerEditorUI.Controls.CommandTemplatePickerControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:KeyboardManagerEditorUI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:KeyboardManagerEditorUI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<DataTemplate x:Key="TextParamTemplate" x:DataType="vm:TemplateParameterViewModel">
|
||||
<TextBox
|
||||
Header="{x:Bind Label}"
|
||||
Text="{x:Bind Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ComboParamTemplate" x:DataType="vm:TemplateParameterViewModel">
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="{x:Bind Label}"
|
||||
ItemsSource="{x:Bind Choices}"
|
||||
SelectedItem="{x:Bind SelectedChoice, Mode=TwoWay}"
|
||||
DisplayMemberPath="DisplayText" />
|
||||
</DataTemplate>
|
||||
|
||||
<local:TemplateParameterSelector
|
||||
x:Key="ParamSelector"
|
||||
ComboTemplate="{StaticResource ComboParamTemplate}"
|
||||
TextTemplate="{StaticResource TextParamTemplate}" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<StackPanel Orientation="Vertical" Spacing="12">
|
||||
<TextBlock
|
||||
x:Name="SelectionDescriptionText"
|
||||
FontStyle="Italic"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.SelectionDescription, Mode=OneWay}" />
|
||||
|
||||
<Rectangle
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<ItemsControl
|
||||
ItemsSource="{x:Bind ViewModel.CurrentParameters, Mode=OneWay}"
|
||||
ItemTemplateSelector="{StaticResource ParamSelector}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical" Spacing="12" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
|
||||
<Rectangle
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<TextBlock x:Uid="TemplatePreviewLabel" FontWeight="SemiBold" />
|
||||
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
IsTextSelectionEnabled="True"
|
||||
Text="{x:Bind ViewModel.ResolvedCommandLine, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<InfoBar
|
||||
x:Name="MissingTemplateInfoBar"
|
||||
x:Uid="MissingTemplateInfoBar"
|
||||
IsClosable="False"
|
||||
IsOpen="False"
|
||||
Severity="Warning">
|
||||
<StackPanel
|
||||
Margin="0,0,0,8"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button
|
||||
x:Name="MissingTemplateKeepButton"
|
||||
x:Uid="TemplateMissingKeepButton"
|
||||
Click="MissingTemplateKeepButton_Click" />
|
||||
</StackPanel>
|
||||
</InfoBar>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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 KeyboardManagerEditorUI.Helpers;
|
||||
using KeyboardManagerEditorUI.Templates;
|
||||
using KeyboardManagerEditorUI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
public sealed partial class CommandTemplatePickerControl : UserControl
|
||||
{
|
||||
public CommandTemplatePickerControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
ViewModel = new CommandTemplatePickerViewModel();
|
||||
|
||||
// Re-raise SelectionChanged when parameter validity could have changed so the host
|
||||
// (UnifiedMappingControl/MainPage) re-evaluates Save-button enablement, not just on
|
||||
// the initial template pick.
|
||||
ViewModel.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(CommandTemplatePickerViewModel.IsAllValid))
|
||||
{
|
||||
SelectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public CommandTemplatePickerViewModel ViewModel { get; }
|
||||
|
||||
public event EventHandler? SelectionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a template is selected and all its required
|
||||
/// parameters have values. Drives Save-button enablement for the RunTemplate action.
|
||||
/// </summary>
|
||||
public bool IsTemplateInputValid =>
|
||||
ViewModel.SelectedTemplate is not null && ViewModel.IsAllValid;
|
||||
|
||||
public event EventHandler? MissingTemplateKeepRequested;
|
||||
|
||||
public TemplateResolver.Resolved? ResolveCurrent()
|
||||
{
|
||||
if (ViewModel.SelectedTemplate is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return TemplateResolver.Resolve(
|
||||
ViewModel.SelectedTemplate,
|
||||
ViewModel.CollectParameterValues());
|
||||
}
|
||||
|
||||
public string? CurrentTemplateId => ViewModel.SelectedTemplate?.Id;
|
||||
|
||||
public Dictionary<string, string> CurrentParameterValues => ViewModel.CollectParameterValues();
|
||||
|
||||
public void LoadExisting(string templateId, IReadOnlyDictionary<string, string>? values)
|
||||
{
|
||||
try
|
||||
{
|
||||
ViewModel.LoadExisting(templateId, values);
|
||||
MissingTemplateInfoBar.IsOpen = false;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
ShowMissingTemplateInfoBar();
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
ViewModel.Clear();
|
||||
MissingTemplateInfoBar.IsOpen = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a command template by id (driven by the host action menu) and notifies listeners.
|
||||
/// </summary>
|
||||
public void SelectCommand(string templateId)
|
||||
{
|
||||
ViewModel.SelectTemplate(templateId);
|
||||
MissingTemplateInfoBar.IsOpen = false;
|
||||
SelectionChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name of the currently selected command (empty when none selected).
|
||||
/// </summary>
|
||||
public string CurrentCommandDisplay =>
|
||||
ViewModel.SelectedTemplate is { } t
|
||||
? ResourceHelper.GetString(t.DisplayResourceKey)
|
||||
: string.Empty;
|
||||
|
||||
private void ShowMissingTemplateInfoBar()
|
||||
{
|
||||
MissingTemplateInfoBar.IsOpen = true;
|
||||
}
|
||||
|
||||
private void MissingTemplateKeepButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MissingTemplateInfoBar.IsOpen = false;
|
||||
MissingTemplateKeepRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using KeyboardManagerEditorUI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
public sealed partial class TemplateParameterSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate? TextTemplate { get; set; }
|
||||
|
||||
public DataTemplate? ComboTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
if (item is TemplateParameterViewModel vm)
|
||||
{
|
||||
return vm.Type switch
|
||||
{
|
||||
"Combo" => ComboTemplate ?? TextTemplate!,
|
||||
_ => TextTemplate!,
|
||||
};
|
||||
}
|
||||
|
||||
return TextTemplate!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,52 +175,67 @@
|
||||
Orientation="Vertical"
|
||||
Spacing="8">
|
||||
<TextBlock x:Uid="ActionLabel" FontWeight="SemiBold" />
|
||||
<ComboBox
|
||||
x:Name="ActionTypeComboBox"
|
||||
<DropDownButton
|
||||
x:Name="ActionTypeButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="ActionTypeComboBox_SelectionChanged">
|
||||
<ComboBoxItem
|
||||
x:Uid="ActionType_KeyOrShortcut"
|
||||
IsSelected="True"
|
||||
Tag="KeyOrShortcut">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionType_KeyOrShortcut_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem x:Uid="ActionType_Text" Tag="Text">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionType_Text_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem x:Uid="ActionType_OpenUrl" Tag="OpenUrl">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionType_OpenUrl_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem x:Uid="ActionType_OpenApp" Tag="OpenApp">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionType_OpenApp_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem x:Uid="ActionType_Disable" Tag="Disable">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionType_Disable_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<!--
|
||||
<ComboBoxItem x:Uid="ActionType_MouseClick" Tag="MouseClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionType_MouseClick_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
-->
|
||||
</ComboBox>
|
||||
HorizontalContentAlignment="Left">
|
||||
<DropDownButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
x:Name="ActionItem_KeyOrShortcut"
|
||||
x:Uid="ActionType_KeyOrShortcut_Text"
|
||||
Click="OnActionTypeClick"
|
||||
Tag="KeyOrShortcut">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Name="ActionItem_Text"
|
||||
x:Uid="ActionType_Text_Text"
|
||||
Click="OnActionTypeClick"
|
||||
Tag="Text">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Name="ActionItem_OpenUrl"
|
||||
x:Uid="ActionType_OpenUrl_Text"
|
||||
Click="OnActionTypeClick"
|
||||
Tag="OpenUrl">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Name="ActionItem_OpenApp"
|
||||
x:Uid="ActionType_OpenApp_Text"
|
||||
Click="OnActionTypeClick"
|
||||
Tag="OpenApp">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Name="ActionItem_Disable"
|
||||
x:Uid="ActionType_Disable_Text"
|
||||
Click="OnActionTypeClick"
|
||||
Tag="Disable">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutSubItem
|
||||
x:Name="RunPtCommandSubItem"
|
||||
x:Uid="ActionType_RunTemplate_Text">
|
||||
<MenuFlyoutSubItem.Icon>
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
</MenuFlyoutSubItem.Icon>
|
||||
</MenuFlyoutSubItem>
|
||||
</MenuFlyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="0,12,0,12"
|
||||
@@ -229,7 +244,7 @@
|
||||
<tkcontrols:SwitchPresenter
|
||||
x:Name="ActionSwitchPresenter"
|
||||
TargetType="x:String"
|
||||
Value="{Binding SelectedItem.Tag, ElementName=ActionTypeComboBox}">
|
||||
Value="KeyOrShortcut">
|
||||
<!-- Key or Shortcut Action -->
|
||||
<tkcontrols:Case Value="KeyOrShortcut">
|
||||
<ToggleButton
|
||||
@@ -398,6 +413,12 @@
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</tkcontrols:Case>
|
||||
<!-- Run From Template Action -->
|
||||
<tkcontrols:Case Value="RunTemplate">
|
||||
<local:CommandTemplatePickerControl
|
||||
x:Name="TemplatePicker"
|
||||
MissingTemplateKeepRequested="TemplatePicker_MissingTemplateKeepRequested" />
|
||||
</tkcontrols:Case>
|
||||
</tkcontrols:SwitchPresenter>
|
||||
</StackPanel>
|
||||
<!-- Validation InfoBar spanning all columns -->
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using KeyboardManagerEditorUI.Helpers;
|
||||
using KeyboardManagerEditorUI.Interop;
|
||||
using KeyboardManagerEditorUI.Templates;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Storage;
|
||||
@@ -44,6 +45,13 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
private bool _urlPathDirty;
|
||||
private bool _programPathDirty;
|
||||
|
||||
private string _currentActionTag = "KeyOrShortcut";
|
||||
|
||||
// Resolved program path/args captured when loading a template mapping, so the
|
||||
// missing-template "Keep as plain command" path can preserve the command.
|
||||
private string _templateFallbackProgramPath = string.Empty;
|
||||
private string _templateFallbackProgramArgs = string.Empty;
|
||||
|
||||
public bool AllowChords { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
@@ -79,6 +87,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
OpenApp,
|
||||
MouseClick,
|
||||
Disable,
|
||||
RunTemplate,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -119,26 +128,16 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
/// <summary>
|
||||
/// Gets the current action type.
|
||||
/// </summary>
|
||||
public ActionType CurrentActionType
|
||||
public ActionType CurrentActionType => _currentActionTag switch
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ActionTypeComboBox?.SelectedItem is ComboBoxItem item)
|
||||
{
|
||||
return item.Tag?.ToString() switch
|
||||
{
|
||||
"Text" => ActionType.Text,
|
||||
"OpenUrl" => ActionType.OpenUrl,
|
||||
"OpenApp" => ActionType.OpenApp,
|
||||
"MouseClick" => ActionType.MouseClick,
|
||||
"Disable" => ActionType.Disable,
|
||||
_ => ActionType.KeyOrShortcut,
|
||||
};
|
||||
}
|
||||
|
||||
return ActionType.KeyOrShortcut;
|
||||
}
|
||||
}
|
||||
"Text" => ActionType.Text,
|
||||
"OpenUrl" => ActionType.OpenUrl,
|
||||
"OpenApp" => ActionType.OpenApp,
|
||||
"MouseClick" => ActionType.MouseClick,
|
||||
"Disable" => ActionType.Disable,
|
||||
"RunTemplate" => ActionType.RunTemplate,
|
||||
_ => ActionType.KeyOrShortcut,
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -163,6 +162,8 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
RaiseValidationStateChanged();
|
||||
};
|
||||
|
||||
BuildRunPtCommandMenu();
|
||||
|
||||
this.Unloaded += UnifiedMappingControl_Unloaded;
|
||||
}
|
||||
|
||||
@@ -172,24 +173,49 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
|
||||
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Set up event handlers for app-specific checkbox
|
||||
// Set up event handlers for app-specific checkbox.
|
||||
// Detach first so re-entering the visual tree (dialog reopened) does not stack handlers.
|
||||
AppSpecificCheckBox.Checked -= AppSpecificCheckBox_Changed;
|
||||
AppSpecificCheckBox.Unchecked -= AppSpecificCheckBox_Changed;
|
||||
AppSpecificCheckBox.Checked += AppSpecificCheckBox_Changed;
|
||||
AppSpecificCheckBox.Unchecked += AppSpecificCheckBox_Changed;
|
||||
|
||||
// Wire template picker selection/validity changes so validation re-runs when the user
|
||||
// picks a template or edits its parameters. Guarded against duplicate subscriptions.
|
||||
if (TemplatePicker != null)
|
||||
{
|
||||
TemplatePicker.SelectionChanged -= TemplatePicker_SelectionChanged;
|
||||
TemplatePicker.SelectionChanged += TemplatePicker_SelectionChanged;
|
||||
}
|
||||
|
||||
// Activate keyboard hook for the trigger input
|
||||
if (TriggerKeyToggleBtn.IsChecked == true)
|
||||
{
|
||||
_currentInputMode = KeyInputMode.OriginalKeys;
|
||||
KeyboardHookHelper.Instance.ActivateHook(this);
|
||||
}
|
||||
|
||||
// Initialize the action button label here (not in the constructor) so the flyout
|
||||
// items' localized Text is guaranteed populated before it is read.
|
||||
UpdateActionButtonContent(_currentActionTag);
|
||||
}
|
||||
|
||||
private void UnifiedMappingControl_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Detach handlers wired in UserControl_Loaded so they don't accumulate across reopens.
|
||||
AppSpecificCheckBox.Checked -= AppSpecificCheckBox_Changed;
|
||||
AppSpecificCheckBox.Unchecked -= AppSpecificCheckBox_Changed;
|
||||
if (TemplatePicker != null)
|
||||
{
|
||||
TemplatePicker.SelectionChanged -= TemplatePicker_SelectionChanged;
|
||||
}
|
||||
|
||||
Reset();
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
|
||||
private void TemplatePicker_SelectionChanged(object? sender, EventArgs e) => RaiseValidationStateChanged();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Trigger Type Handling
|
||||
@@ -242,29 +268,128 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
|
||||
#region Action Type Handling
|
||||
|
||||
private void ActionTypeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
private void OnActionTypeClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ActionTypeComboBox?.SelectedItem is ComboBoxItem item)
|
||||
if (sender is FrameworkElement fe && fe.Tag is string tag)
|
||||
{
|
||||
string? tag = item.Tag?.ToString();
|
||||
SetActionTag(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup keyboard hook when switching away from key/shortcut
|
||||
if (tag != "KeyOrShortcut")
|
||||
private void OnCommandClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuFlyoutItem item && item.Tag is string templateId)
|
||||
{
|
||||
// Select the template first so the action-type switch validates against the picked
|
||||
// command (not the previously-selected one), avoiding a transient stale Save state.
|
||||
TemplatePicker?.SelectCommand(templateId);
|
||||
SetActionTag("RunTemplate");
|
||||
UpdateActionButtonContent("RunTemplate", item.Text);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetActionTag(string tag)
|
||||
{
|
||||
_currentActionTag = tag;
|
||||
|
||||
if (ActionSwitchPresenter != null)
|
||||
{
|
||||
ActionSwitchPresenter.Value = tag;
|
||||
}
|
||||
|
||||
// Cleanup keyboard hook when switching away from key/shortcut
|
||||
if (tag != "KeyOrShortcut")
|
||||
{
|
||||
if (_currentInputMode == KeyInputMode.RemappedKeys)
|
||||
{
|
||||
if (_currentInputMode == KeyInputMode.RemappedKeys)
|
||||
{
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
|
||||
if (ActionKeyToggleBtn?.IsChecked == true)
|
||||
{
|
||||
ActionKeyToggleBtn.IsChecked = false;
|
||||
}
|
||||
if (ActionKeyToggleBtn?.IsChecked == true)
|
||||
{
|
||||
ActionKeyToggleBtn.IsChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
HideValidationMessage();
|
||||
RaiseValidationStateChanged();
|
||||
UpdateActionButtonContent(tag);
|
||||
}
|
||||
|
||||
private void UpdateActionButtonContent(string tag, string? commandDisplay = null)
|
||||
{
|
||||
if (ActionTypeButton == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string cmd = commandDisplay ?? TemplatePicker?.CurrentCommandDisplay ?? string.Empty;
|
||||
|
||||
string text = tag switch
|
||||
{
|
||||
"Text" => ActionItem_Text.Text,
|
||||
"OpenUrl" => ActionItem_OpenUrl.Text,
|
||||
"OpenApp" => ActionItem_OpenApp.Text,
|
||||
"Disable" => ActionItem_Disable.Text,
|
||||
"RunTemplate" => string.IsNullOrEmpty(cmd)
|
||||
? RunPtCommandSubItem.Text
|
||||
: $"{RunPtCommandSubItem.Text}: {cmd}",
|
||||
_ => ActionItem_KeyOrShortcut.Text,
|
||||
};
|
||||
|
||||
ActionTypeButton.Content = text;
|
||||
}
|
||||
|
||||
private void BuildRunPtCommandMenu()
|
||||
{
|
||||
// A malformed/missing catalog must not crash construction of the whole mapping editor.
|
||||
// Degrade gracefully: log and leave the "Run PowerToys command" submenu empty/disabled.
|
||||
try
|
||||
{
|
||||
foreach (var module in CommandTemplateCatalog.Instance.Data.Modules)
|
||||
{
|
||||
var moduleSub = new MenuFlyoutSubItem
|
||||
{
|
||||
Text = ResourceHelper.GetString(module.DisplayResourceKey),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(module.IconGlyph))
|
||||
{
|
||||
moduleSub.Icon = new FontIcon { Glyph = module.IconGlyph };
|
||||
}
|
||||
|
||||
foreach (var cmd in module.Commands)
|
||||
{
|
||||
var item = new MenuFlyoutItem
|
||||
{
|
||||
Text = ResourceHelper.GetString(cmd.DisplayResourceKey),
|
||||
Tag = cmd.Id,
|
||||
};
|
||||
item.Click += OnCommandClick;
|
||||
moduleSub.Items.Add(item);
|
||||
}
|
||||
|
||||
RunPtCommandSubItem.Items.Add(moduleSub);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ManagedCommon.Logger.LogError($"Failed to build PowerToys command template menu: {ex.Message}");
|
||||
RunPtCommandSubItem.IsEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TemplatePicker_MissingTemplateKeepRequested(object sender, EventArgs e)
|
||||
{
|
||||
// The user chose to keep the resolved command but discard the template association.
|
||||
// Switch to OpenApp first so the program-path inputs are realized, then populate them
|
||||
// with the previously-resolved command so the mapping is preserved (not blanked).
|
||||
SetActionType(ActionType.OpenApp);
|
||||
SetProgramPath(_templateFallbackProgramPath);
|
||||
SetProgramArgs(_templateFallbackProgramArgs);
|
||||
|
||||
// Clear the picker so it doesn't retain stale template state.
|
||||
TemplatePicker?.Reset();
|
||||
}
|
||||
|
||||
private void ActionKeyToggleBtn_Checked(object sender, RoutedEventArgs e)
|
||||
@@ -798,6 +923,29 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
/// </summary>
|
||||
public ProgramAlreadyRunningAction GetIfRunningAction() => (ProgramAlreadyRunningAction)(IfRunningComboBox?.SelectedIndex ?? 0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resolved template executable (for RunTemplate action type).
|
||||
/// Returns null if no template is selected or the template cannot be resolved.
|
||||
/// </summary>
|
||||
public string? GetResolvedTemplateExecutable() => TemplatePicker?.ResolveCurrent()?.Executable;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resolved template arguments (for RunTemplate action type).
|
||||
/// Returns null if no template is selected or the template cannot be resolved.
|
||||
/// </summary>
|
||||
public string? GetResolvedTemplateArgs() => TemplatePicker?.ResolveCurrent()?.Args;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the template id of the currently selected template (for RunTemplate action type).
|
||||
/// Returns null if no template is selected.
|
||||
/// </summary>
|
||||
public string? GetCurrentTemplateId() => TemplatePicker?.CurrentTemplateId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current template parameter values (for RunTemplate action type).
|
||||
/// </summary>
|
||||
public Dictionary<string, string> GetCurrentTemplateParameterValues() => TemplatePicker?.CurrentParameterValues ?? new Dictionary<string, string>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API - Validation
|
||||
@@ -820,6 +968,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
ActionType.OpenUrl => !string.IsNullOrWhiteSpace(UrlPathInput?.Text),
|
||||
ActionType.OpenApp => !string.IsNullOrWhiteSpace(ProgramPathInput?.Text),
|
||||
ActionType.Disable => true,
|
||||
ActionType.RunTemplate => TemplatePicker?.IsTemplateInputValid == true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
@@ -865,11 +1014,6 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
/// </summary>
|
||||
public void SetActionType(ActionType actionType)
|
||||
{
|
||||
if (ActionTypeComboBox == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string tag = actionType switch
|
||||
{
|
||||
ActionType.Text => "Text",
|
||||
@@ -877,17 +1021,11 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
ActionType.OpenApp => "OpenApp",
|
||||
ActionType.Disable => "Disable",
|
||||
ActionType.MouseClick => "MouseClick",
|
||||
ActionType.RunTemplate => "RunTemplate",
|
||||
_ => "KeyOrShortcut",
|
||||
};
|
||||
|
||||
foreach (var item in ActionTypeComboBox.Items)
|
||||
{
|
||||
if (item is ComboBoxItem comboBoxItem && comboBoxItem.Tag is string itemTag && itemTag == tag)
|
||||
{
|
||||
ActionTypeComboBox.SelectedItem = comboBoxItem;
|
||||
return;
|
||||
}
|
||||
}
|
||||
SetActionTag(tag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -978,6 +1116,26 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an existing template mapping into the picker (for RunTemplate action type).
|
||||
/// Switches the action type to RunTemplate and calls LoadExisting on the picker.
|
||||
/// </summary>
|
||||
public void SetRunTemplate(
|
||||
string templateId,
|
||||
IReadOnlyDictionary<string, string>? parameterValues,
|
||||
string fallbackProgramPath = "",
|
||||
string fallbackProgramArgs = "")
|
||||
{
|
||||
// Remember the already-resolved command so "Keep as plain command" can preserve it
|
||||
// if the stored templateId is no longer in the catalog.
|
||||
_templateFallbackProgramPath = fallbackProgramPath ?? string.Empty;
|
||||
_templateFallbackProgramArgs = fallbackProgramArgs ?? string.Empty;
|
||||
|
||||
SetActionType(ActionType.RunTemplate);
|
||||
TemplatePicker?.LoadExisting(templateId, parameterValues);
|
||||
UpdateActionButtonContent("RunTemplate");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the mapping is app-specific.
|
||||
/// </summary>
|
||||
@@ -1121,10 +1279,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
TriggerTypeComboBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
if (ActionTypeComboBox != null)
|
||||
{
|
||||
ActionTypeComboBox.SelectedIndex = 0;
|
||||
}
|
||||
SetActionTag("KeyOrShortcut");
|
||||
|
||||
if (MouseTriggerComboBox != null)
|
||||
{
|
||||
@@ -1186,6 +1341,13 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
VisibilityComboBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// Reset template picker
|
||||
TemplatePicker?.Reset();
|
||||
|
||||
// Clear the cached missing-template fallback command so it can't leak into the next open.
|
||||
_templateFallbackProgramPath = string.Empty;
|
||||
_templateFallbackProgramArgs = string.Empty;
|
||||
|
||||
HideValidationMessage();
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,9 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? startDirectory = null,
|
||||
int elevation = 0,
|
||||
int ifRunningAction = 0,
|
||||
int visibility = 0);
|
||||
int visibility = 0,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? templateId = null,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? templateParametersJson = null);
|
||||
|
||||
// Delete Mapping Functions
|
||||
[DllImport(DllName, CallingConvention = Convention)]
|
||||
@@ -165,6 +167,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
public IntPtr ProgramPath;
|
||||
public IntPtr ProgramArgs;
|
||||
public IntPtr UriToOpen;
|
||||
public IntPtr TemplateId;
|
||||
public IntPtr TemplateParametersJson;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
|
||||
@@ -7,7 +7,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using KeyboardManagerEditorUI.Templates;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Interop
|
||||
@@ -61,6 +63,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
var mapping = default(ShortcutMapping);
|
||||
if (KeyboardManagerInterop.GetShortcutRemap(_configHandle, i, ref mapping))
|
||||
{
|
||||
var (templateId, templateParameters) = ReadTemplateFields(ref mapping);
|
||||
result.Add(new ShortcutKeyMapping
|
||||
{
|
||||
OriginalKeys = KeyboardManagerInterop.GetStringAndFree(mapping.OriginalKeys),
|
||||
@@ -71,6 +74,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
|
||||
ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
|
||||
UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
|
||||
TemplateId = templateId,
|
||||
TemplateParameters = templateParameters,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -78,6 +83,33 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
return result;
|
||||
}
|
||||
|
||||
// Reads (and frees) the template metadata strings returned by the native layer.
|
||||
// Must be called for every mapping so the native-allocated strings are always freed.
|
||||
private static (string? TemplateId, Dictionary<string, string>? TemplateParameters) ReadTemplateFields(ref ShortcutMapping mapping)
|
||||
{
|
||||
string templateId = KeyboardManagerInterop.GetStringAndFree(mapping.TemplateId);
|
||||
string templateParametersJson = KeyboardManagerInterop.GetStringAndFree(mapping.TemplateParametersJson);
|
||||
|
||||
Dictionary<string, string>? parameters = null;
|
||||
if (!string.IsNullOrEmpty(templateParametersJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
parameters = JsonSerializer.Deserialize(
|
||||
templateParametersJson,
|
||||
CommandTemplateJsonContext.Default.DictionaryStringString);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Malformed on-disk metadata must never break config load — the two template
|
||||
// strings are already freed above; swallow so the caller still frees the rest.
|
||||
parameters = null;
|
||||
}
|
||||
}
|
||||
|
||||
return (string.IsNullOrEmpty(templateId) ? null : templateId, parameters);
|
||||
}
|
||||
|
||||
public List<ShortcutKeyMapping> GetShortcutMappingsByType(ShortcutOperationType operationType)
|
||||
{
|
||||
var result = new List<ShortcutKeyMapping>();
|
||||
@@ -88,6 +120,7 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
var mapping = default(ShortcutMapping);
|
||||
if (KeyboardManagerInterop.GetShortcutRemapByType(_configHandle, (int)operationType, i, ref mapping))
|
||||
{
|
||||
var (templateId, templateParameters) = ReadTemplateFields(ref mapping);
|
||||
result.Add(new ShortcutKeyMapping
|
||||
{
|
||||
OriginalKeys = KeyboardManagerInterop.GetStringAndFree(mapping.OriginalKeys),
|
||||
@@ -98,6 +131,8 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
|
||||
ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
|
||||
UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
|
||||
TemplateId = templateId,
|
||||
TemplateParameters = templateParameters,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -221,6 +256,14 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
|
||||
if (shortcutKeyMapping.OperationType == ShortcutOperationType.RunProgram)
|
||||
{
|
||||
string? templateParametersJson = null;
|
||||
if (shortcutKeyMapping.TemplateParameters is not null && shortcutKeyMapping.TemplateParameters.Count > 0)
|
||||
{
|
||||
templateParametersJson = JsonSerializer.Serialize(
|
||||
shortcutKeyMapping.TemplateParameters,
|
||||
CommandTemplateJsonContext.Default.DictionaryStringString);
|
||||
}
|
||||
|
||||
return KeyboardManagerInterop.AddShortcutRemap(
|
||||
_configHandle,
|
||||
shortcutKeyMapping.OriginalKeys,
|
||||
@@ -232,7 +275,9 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
string.IsNullOrEmpty(shortcutKeyMapping.StartInDirectory) ? null : shortcutKeyMapping.StartInDirectory,
|
||||
(int)shortcutKeyMapping.Elevation,
|
||||
(int)shortcutKeyMapping.IfRunningAction,
|
||||
(int)shortcutKeyMapping.Visibility);
|
||||
(int)shortcutKeyMapping.Visibility,
|
||||
shortcutKeyMapping.TemplateId,
|
||||
templateParametersJson);
|
||||
}
|
||||
else if (shortcutKeyMapping.OperationType == ShortcutOperationType.OpenUri)
|
||||
{
|
||||
|
||||
@@ -36,6 +36,18 @@ namespace KeyboardManagerEditorUI.Interop
|
||||
|
||||
public string UriToOpen { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When non-null, indicates the mapping was created from a command template.
|
||||
/// Used to re-open the template picker on edit.
|
||||
/// </summary>
|
||||
public string? TemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameter values captured at save time for a template-based mapping.
|
||||
/// Null when <see cref="TemplateId"/> is null.
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? TemplateParameters { get; set; }
|
||||
|
||||
public enum ElevationLevel
|
||||
{
|
||||
NonElevated = 0,
|
||||
|
||||
@@ -76,6 +76,11 @@
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Templates\powertoyscli.json">
|
||||
<LogicalName>KeyboardManagerEditorUI.Templates.powertoyscli.json</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Colors.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -260,13 +260,16 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
|
||||
UnifiedMappingControl.Reset();
|
||||
UnifiedMappingControl.SetTriggerKeys(programShortcut.Shortcut.ToList());
|
||||
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.OpenApp);
|
||||
UnifiedMappingControl.SetProgramPath(programShortcut.AppToRun);
|
||||
UnifiedMappingControl.SetProgramArgs(programShortcut.Args);
|
||||
|
||||
// Check if this program shortcut was originally created from a command template.
|
||||
string? templateId = null;
|
||||
Dictionary<string, string>? templateParameters = null;
|
||||
if (!string.IsNullOrEmpty(programShortcut.Id) &&
|
||||
SettingsManager.EditorSettings.ShortcutSettingsDictionary.TryGetValue(programShortcut.Id, out var settings))
|
||||
{
|
||||
templateId = settings.Shortcut.TemplateId;
|
||||
templateParameters = settings.Shortcut.TemplateParameters;
|
||||
|
||||
var mapping = settings.Shortcut;
|
||||
UnifiedMappingControl.SetStartInDirectory(mapping.StartInDirectory);
|
||||
UnifiedMappingControl.SetElevationLevel(mapping.Elevation);
|
||||
@@ -274,6 +277,20 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
UnifiedMappingControl.SetIfRunningAction(mapping.IfRunningAction);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(templateId))
|
||||
{
|
||||
// Restore as RunTemplate: the picker handles missing-template degradation internally.
|
||||
// Pass the resolved command so "Keep as plain command" can preserve it if the
|
||||
// stored templateId is no longer present in the catalog.
|
||||
UnifiedMappingControl.SetRunTemplate(templateId, templateParameters, programShortcut.AppToRun, programShortcut.Args);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.OpenApp);
|
||||
UnifiedMappingControl.SetProgramPath(programShortcut.AppToRun);
|
||||
UnifiedMappingControl.SetProgramArgs(programShortcut.Args);
|
||||
}
|
||||
|
||||
UnifiedMappingControl.SetAppSpecific(!programShortcut.IsAllApps, programShortcut.AppName);
|
||||
RemappingDialog.Title = ResourceHelper.GetString("RemappingDialog_TitleEdit");
|
||||
await ShowRemappingDialog();
|
||||
@@ -394,6 +411,7 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
UnifiedMappingControl.ActionType.OpenUrl => SaveUrlMapping(triggerKeys),
|
||||
UnifiedMappingControl.ActionType.OpenApp => SaveProgramMapping(triggerKeys),
|
||||
UnifiedMappingControl.ActionType.Disable => SaveDisableMapping(triggerKeys),
|
||||
UnifiedMappingControl.ActionType.RunTemplate => SaveRunTemplateMapping(triggerKeys),
|
||||
UnifiedMappingControl.ActionType.MouseClick => throw new NotImplementedException("Mouse click remapping is not yet supported."),
|
||||
_ => false,
|
||||
};
|
||||
@@ -439,6 +457,8 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
triggerKeys, UnifiedMappingControl.GetProgramPath(), isAppSpecific, appName, _mappingService!, _isEditMode),
|
||||
UnifiedMappingControl.ActionType.Disable => ValidationHelper.ValidateDisableMapping(
|
||||
triggerKeys, isAppSpecific, appName, _mappingService!, _isEditMode, editingRemapping),
|
||||
UnifiedMappingControl.ActionType.RunTemplate => ValidationHelper.ValidateAppMapping(
|
||||
triggerKeys, UnifiedMappingControl.GetResolvedTemplateExecutable() ?? string.Empty, isAppSpecific, appName, _mappingService!, _isEditMode),
|
||||
_ => ValidationErrorType.NoError,
|
||||
};
|
||||
}
|
||||
@@ -683,6 +703,51 @@ namespace KeyboardManagerEditorUI.Pages
|
||||
return saved;
|
||||
}
|
||||
|
||||
private bool SaveRunTemplateMapping(List<string> triggerKeys)
|
||||
{
|
||||
string? programPath = UnifiedMappingControl.GetResolvedTemplateExecutable();
|
||||
if (string.IsNullOrEmpty(programPath))
|
||||
{
|
||||
// No template selected or unresolvable — validation should have caught this.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retarget the template's default per-user path to a machine-wide install when needed,
|
||||
// so the saved mapping launches regardless of how PowerToys was installed.
|
||||
programPath = Templates.PowerToysInstallResolver.ResolveExecutable(programPath);
|
||||
|
||||
string originalKeysString = string.Join(";", triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
// Preserve the run-options that were loaded onto the control when editing an existing
|
||||
// template mapping so re-saving does not silently reset them to defaults.
|
||||
var templateParameters = UnifiedMappingControl.GetCurrentTemplateParameterValues();
|
||||
|
||||
var shortcutKeyMapping = new ShortcutKeyMapping
|
||||
{
|
||||
OperationType = ShortcutOperationType.RunProgram,
|
||||
OriginalKeys = originalKeysString,
|
||||
TargetKeys = originalKeysString,
|
||||
ProgramPath = programPath,
|
||||
ProgramArgs = UnifiedMappingControl.GetResolvedTemplateArgs() ?? string.Empty,
|
||||
StartInDirectory = UnifiedMappingControl.GetStartInDirectory(),
|
||||
IfRunningAction = UnifiedMappingControl.GetIfRunningAction(),
|
||||
Visibility = UnifiedMappingControl.GetVisibility(),
|
||||
Elevation = UnifiedMappingControl.GetElevationLevel(),
|
||||
TargetApp = UnifiedMappingControl.GetIsAppSpecific() ? UnifiedMappingControl.GetAppName() : string.Empty,
|
||||
TemplateId = UnifiedMappingControl.GetCurrentTemplateId(),
|
||||
TemplateParameters = templateParameters.Count > 0 ? templateParameters : null,
|
||||
};
|
||||
|
||||
bool saved = _mappingService!.AddShortcutMapping(shortcutKeyMapping);
|
||||
if (saved)
|
||||
{
|
||||
_mappingService.SaveSettings();
|
||||
SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
|
||||
}
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Delete Handlers
|
||||
|
||||
@@ -501,4 +501,52 @@
|
||||
<data name="CheckServiceBtn.Content" xml:space="preserve">
|
||||
<value>Check service status</value>
|
||||
</data>
|
||||
<data name="ActionType_RunTemplate_Text.Text" xml:space="preserve">
|
||||
<value>Run PowerToys Command</value>
|
||||
</data>
|
||||
<data name="TemplatePreviewLabel.Text" xml:space="preserve">
|
||||
<value>Preview</value>
|
||||
</data>
|
||||
<data name="MissingTemplateInfoBar.Title" xml:space="preserve">
|
||||
<value>Template no longer available</value>
|
||||
</data>
|
||||
<data name="MissingTemplateInfoBar.Message" xml:space="preserve">
|
||||
<value>The template originally used for this mapping is no longer in the catalog.</value>
|
||||
</data>
|
||||
<data name="TemplateMissingKeepButton.Content" xml:space="preserve">
|
||||
<value>Keep as plain command</value>
|
||||
</data>
|
||||
<data name="TemplateModule_Settings" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="TemplateCmd_Settings_OpenMain" xml:space="preserve">
|
||||
<value>Open Settings</value>
|
||||
</data>
|
||||
<data name="TemplateCmd_Settings_OpenModule" xml:space="preserve">
|
||||
<value>Open Settings for module...</value>
|
||||
</data>
|
||||
<data name="TemplateParam_Module" xml:space="preserve">
|
||||
<value>Module</value>
|
||||
</data>
|
||||
<data name="Module_ColorPicker" xml:space="preserve">
|
||||
<value>Color Picker</value>
|
||||
</data>
|
||||
<data name="Module_FancyZones" xml:space="preserve">
|
||||
<value>FancyZones</value>
|
||||
</data>
|
||||
<data name="Module_KeyboardManager" xml:space="preserve">
|
||||
<value>Keyboard Manager</value>
|
||||
</data>
|
||||
<data name="Module_PowerLauncher" xml:space="preserve">
|
||||
<value>PowerToys Run</value>
|
||||
</data>
|
||||
<data name="Module_Hosts" xml:space="preserve">
|
||||
<value>Hosts File Editor</value>
|
||||
</data>
|
||||
<data name="Module_RegistryPreview" xml:space="preserve">
|
||||
<value>Registry Preview</value>
|
||||
</data>
|
||||
<data name="Module_ZoomIt" xml:space="preserve">
|
||||
<value>ZoomIt</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
public sealed class CommandTemplate
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
public string DisplayResourceKey { get; init; } = string.Empty;
|
||||
|
||||
public string Executable { get; init; } = string.Empty;
|
||||
|
||||
public string ArgsTemplate { get; init; } = string.Empty;
|
||||
|
||||
public List<TemplateParameter> Parameters { get; init; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
public sealed class CommandTemplateCatalog
|
||||
{
|
||||
private const string ResourceName = "KeyboardManagerEditorUI.Templates.powertoyscli.json";
|
||||
private const int SupportedSchemaVersion = 1;
|
||||
|
||||
private static readonly Lazy<CommandTemplateCatalog> _instance = new(() => Load());
|
||||
|
||||
public static CommandTemplateCatalog Instance => _instance.Value;
|
||||
|
||||
public PowerToysCliCatalog Data { get; }
|
||||
|
||||
private CommandTemplateCatalog(PowerToysCliCatalog data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
private static CommandTemplateCatalog Load()
|
||||
{
|
||||
var assembly = typeof(CommandTemplateCatalog).Assembly;
|
||||
using var stream = assembly.GetManifestResourceStream(ResourceName)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Embedded resource '{ResourceName}' not found. " +
|
||||
"Check KeyboardManagerEditorUI.csproj <EmbeddedResource> entry.");
|
||||
|
||||
var data = JsonSerializer.Deserialize(
|
||||
stream,
|
||||
CommandTemplateJsonContext.Default.PowerToysCliCatalog)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Failed to deserialize '{ResourceName}' — JsonSerializer returned null.");
|
||||
|
||||
if (data.SchemaVersion < 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid powertoyscli.json schemaVersion={data.SchemaVersion}; " +
|
||||
$"expected >= 1.");
|
||||
}
|
||||
|
||||
if (data.SchemaVersion > SupportedSchemaVersion)
|
||||
{
|
||||
// Newer catalogs are read best-effort: unknown additive fields are ignored by
|
||||
// the deserializer, so a forward-compatible bump must not break older binaries.
|
||||
ManagedCommon.Logger.LogWarning(
|
||||
$"powertoyscli.json schemaVersion={data.SchemaVersion} is newer than " +
|
||||
$"supported {SupportedSchemaVersion}; reading best-effort.");
|
||||
}
|
||||
|
||||
if (data.Modules.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"powertoyscli.json has zero modules — at least one module is required.");
|
||||
}
|
||||
|
||||
return new CommandTemplateCatalog(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
[JsonSerializable(typeof(PowerToysCliCatalog))]
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
[JsonSourceGenerationOptions(
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip)]
|
||||
internal sealed partial class CommandTemplateJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
public sealed class CommandTemplateModule
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
public string DisplayResourceKey { get; init; } = string.Empty;
|
||||
|
||||
public string? IconGlyph { get; init; }
|
||||
|
||||
public List<CommandTemplate> Commands { get; init; } = new();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
public sealed class PowerToysCliCatalog
|
||||
{
|
||||
public int SchemaVersion { get; init; }
|
||||
|
||||
public List<CommandTemplateModule> Modules { get; init; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the on-disk location of a PowerToys executable referenced by a command template.
|
||||
/// Templates ship a per-user (<c>%LOCALAPPDATA%</c>) path by default; this retargets to a
|
||||
/// machine-wide (<c>%ProgramFiles%</c>) install when the per-user path is absent so the saved
|
||||
/// mapping works regardless of install scope.
|
||||
/// </summary>
|
||||
internal static class PowerToysInstallResolver
|
||||
{
|
||||
// Candidate install locations, in preference order. Env-var form is preserved in the saved
|
||||
// value so the engine expands it at trigger time (and it survives a reinstall in place).
|
||||
private static readonly string[] Candidates =
|
||||
{
|
||||
@"%LOCALAPPDATA%\PowerToys\PowerToys.exe",
|
||||
|
||||
// The editor is always a 64-bit process (x64/ARM64; no x86 build), so %ProgramFiles%
|
||||
// already resolves to the native 64-bit Program Files where a machine-wide install lives.
|
||||
@"%ProgramFiles%\PowerToys\PowerToys.exe",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns an executable path that exists on disk. If <paramref name="executable"/> already
|
||||
/// resolves to an existing file it is returned unchanged. For a nonexistent
|
||||
/// <c>PowerToys.exe</c> path, known install locations are probed. If none exist the original
|
||||
/// value is returned (the engine will surface a "program not found" error at trigger time).
|
||||
/// </summary>
|
||||
public static string ResolveExecutable(string executable)
|
||||
{
|
||||
if (string.IsNullOrEmpty(executable))
|
||||
{
|
||||
return executable;
|
||||
}
|
||||
|
||||
if (File.Exists(Environment.ExpandEnvironmentVariables(executable)))
|
||||
{
|
||||
return executable;
|
||||
}
|
||||
|
||||
// Only retarget the known PowerToys.exe; leave arbitrary executables untouched.
|
||||
string fileName = Path.GetFileName(Environment.ExpandEnvironmentVariables(executable));
|
||||
if (!string.Equals(fileName, "PowerToys.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return executable;
|
||||
}
|
||||
|
||||
foreach (var candidate in Candidates)
|
||||
{
|
||||
if (File.Exists(Environment.ExpandEnvironmentVariables(candidate)))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return executable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
public sealed class TemplateChoice
|
||||
{
|
||||
public string Value { get; init; } = string.Empty;
|
||||
|
||||
public string DisplayResourceKey { get; init; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
public sealed class TemplateParameter
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public string LabelResourceKey { get; init; } = string.Empty;
|
||||
|
||||
public string Type { get; init; } = "Text";
|
||||
|
||||
public bool Required { get; init; } = true;
|
||||
|
||||
public List<TemplateChoice>? Choices { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Templates
|
||||
{
|
||||
public static class TemplateResolver
|
||||
{
|
||||
public readonly record struct Resolved(string Executable, string Args);
|
||||
|
||||
// Matches {paramName} placeholders. Resolution is single-pass over the original
|
||||
// template so a substituted value can never be re-interpreted as another placeholder
|
||||
// (prevents substitution-injection once free-text parameters are introduced).
|
||||
private static readonly Regex PlaceholderRegex = new(@"\{(\w+)\}");
|
||||
|
||||
// Characters that force argument quoting per CommandLineToArgvW parsing rules.
|
||||
private static readonly char[] QuoteTriggers = { ' ', '\t', '\n', '\v', '"' };
|
||||
|
||||
public static Resolved Resolve(
|
||||
CommandTemplate template,
|
||||
IReadOnlyDictionary<string, string>? values)
|
||||
{
|
||||
var argsTemplate = template.ArgsTemplate ?? string.Empty;
|
||||
|
||||
// Pre-compute the (quoted-if-needed) replacement for every declared parameter.
|
||||
var substitutions = new Dictionary<string, string>();
|
||||
foreach (var p in template.Parameters)
|
||||
{
|
||||
string raw = string.Empty;
|
||||
if (values is not null && values.TryGetValue(p.Name, out var v))
|
||||
{
|
||||
raw = v ?? string.Empty;
|
||||
}
|
||||
|
||||
substitutions[p.Name] = QuoteArgumentIfNeeded(raw);
|
||||
}
|
||||
|
||||
// Single pass: each {name} is replaced exactly once against the original template.
|
||||
// Unknown placeholders are left untouched (matches prior behavior).
|
||||
string args = PlaceholderRegex.Replace(argsTemplate, m =>
|
||||
substitutions.TryGetValue(m.Groups[1].Value, out var replacement)
|
||||
? replacement
|
||||
: m.Value);
|
||||
|
||||
return new Resolved(template.Executable ?? string.Empty, args);
|
||||
}
|
||||
|
||||
// Quotes a value for a Windows command line (CommandLineToArgvW rules) only when it
|
||||
// contains whitespace or a quote, so simple values (e.g. fixed combo choices) and
|
||||
// empty values pass through unchanged.
|
||||
internal static string QuoteArgumentIfNeeded(string value)
|
||||
{
|
||||
if (value.Length == 0 || value.IndexOfAny(QuoteTriggers) < 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('"');
|
||||
|
||||
int backslashes = 0;
|
||||
foreach (char c in value)
|
||||
{
|
||||
if (c == '\\')
|
||||
{
|
||||
backslashes++;
|
||||
}
|
||||
else if (c == '"')
|
||||
{
|
||||
// Escape the run of backslashes preceding a quote, then the quote itself.
|
||||
sb.Append('\\', (backslashes * 2) + 1);
|
||||
backslashes = 0;
|
||||
sb.Append('"');
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append('\\', backslashes);
|
||||
backslashes = 0;
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Escape trailing backslashes so they don't escape the closing quote.
|
||||
sb.Append('\\', backslashes * 2);
|
||||
sb.Append('"');
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"modules": [
|
||||
{
|
||||
"id": "settings",
|
||||
"displayResourceKey": "TemplateModule_Settings",
|
||||
"iconGlyph": "",
|
||||
"commands": [
|
||||
{
|
||||
"id": "settings.openMain",
|
||||
"displayResourceKey": "TemplateCmd_Settings_OpenMain",
|
||||
"executable": "%LOCALAPPDATA%\\PowerToys\\PowerToys.exe",
|
||||
"argsTemplate": "--open-settings",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"id": "settings.openModule",
|
||||
"displayResourceKey": "TemplateCmd_Settings_OpenModule",
|
||||
"executable": "%LOCALAPPDATA%\\PowerToys\\PowerToys.exe",
|
||||
"argsTemplate": "--open-settings={module}",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "module",
|
||||
"labelResourceKey": "TemplateParam_Module",
|
||||
"type": "Combo",
|
||||
"required": true,
|
||||
"choices": [
|
||||
{ "value": "ColorPicker", "displayResourceKey": "Module_ColorPicker" },
|
||||
{ "value": "FancyZones", "displayResourceKey": "Module_FancyZones" },
|
||||
{ "value": "KeyboardManager", "displayResourceKey": "Module_KeyboardManager" },
|
||||
{ "value": "PowerLauncher", "displayResourceKey": "Module_PowerLauncher" },
|
||||
{ "value": "Hosts", "displayResourceKey": "Module_Hosts" },
|
||||
{ "value": "RegistryPreview", "displayResourceKey": "Module_RegistryPreview" },
|
||||
{ "value": "ZoomIt", "displayResourceKey": "Module_ZoomIt" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using KeyboardManagerEditorUI.Helpers;
|
||||
using KeyboardManagerEditorUI.Templates;
|
||||
|
||||
namespace KeyboardManagerEditorUI.ViewModels
|
||||
{
|
||||
public sealed class CommandTemplatePickerViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private CommandTemplate? _selectedTemplate;
|
||||
private string _selectionDescription = string.Empty;
|
||||
private string _resolvedCommandLine = string.Empty;
|
||||
|
||||
public ObservableCollection<TemplateParameterViewModel> CurrentParameters { get; } = new();
|
||||
|
||||
public CommandTemplate? SelectedTemplate
|
||||
{
|
||||
get => _selectedTemplate;
|
||||
private set
|
||||
{
|
||||
_selectedTemplate = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectionDescription
|
||||
{
|
||||
get => _selectionDescription;
|
||||
private set
|
||||
{
|
||||
_selectionDescription = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string ResolvedCommandLine
|
||||
{
|
||||
get => _resolvedCommandLine;
|
||||
private set
|
||||
{
|
||||
_resolvedCommandLine = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAllValid => CurrentParameters.All(p => p.IsValid);
|
||||
|
||||
public void SelectTemplate(string templateId)
|
||||
{
|
||||
var (module, template) = FindWithModule(templateId);
|
||||
ApplyTemplate(module, template, prefilledValues: null);
|
||||
}
|
||||
|
||||
public void LoadExisting(string templateId, IReadOnlyDictionary<string, string>? values)
|
||||
{
|
||||
var (module, template) = FindWithModule(templateId);
|
||||
if (template is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Template '{templateId}' not found in catalog.");
|
||||
}
|
||||
|
||||
ApplyTemplate(module, template, values);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SelectedTemplate = null;
|
||||
SelectionDescription = string.Empty;
|
||||
ResolvedCommandLine = string.Empty;
|
||||
|
||||
DetachParameterListeners();
|
||||
CurrentParameters.Clear();
|
||||
}
|
||||
|
||||
public Dictionary<string, string> CollectParameterValues()
|
||||
{
|
||||
return CurrentParameters.ToDictionary(p => p.Name, p => p.Value);
|
||||
}
|
||||
|
||||
private (CommandTemplateModule? Module, CommandTemplate? Template) FindWithModule(string templateId)
|
||||
{
|
||||
foreach (var m in CommandTemplateCatalog.Instance.Data.Modules)
|
||||
{
|
||||
var t = m.Commands.FirstOrDefault(c => c.Id == templateId);
|
||||
if (t is not null)
|
||||
{
|
||||
return (m, t);
|
||||
}
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private void ApplyTemplate(
|
||||
CommandTemplateModule? module,
|
||||
CommandTemplate? template,
|
||||
IReadOnlyDictionary<string, string>? prefilledValues)
|
||||
{
|
||||
DetachParameterListeners();
|
||||
CurrentParameters.Clear();
|
||||
|
||||
SelectedTemplate = template;
|
||||
|
||||
if (template is null || module is null)
|
||||
{
|
||||
SelectionDescription = string.Empty;
|
||||
ResolvedCommandLine = string.Empty;
|
||||
OnPropertyChanged(nameof(IsAllValid));
|
||||
return;
|
||||
}
|
||||
|
||||
SelectionDescription =
|
||||
$"{ResourceHelper.GetString(module.DisplayResourceKey)} → {ResourceHelper.GetString(template.DisplayResourceKey)}";
|
||||
|
||||
foreach (var p in template.Parameters)
|
||||
{
|
||||
var vm = new TemplateParameterViewModel(p);
|
||||
if (prefilledValues is not null && prefilledValues.TryGetValue(p.Name, out var v))
|
||||
{
|
||||
if (vm.Choices is not null)
|
||||
{
|
||||
vm.SelectedChoice = vm.Choices.FirstOrDefault(c => c.Value == v);
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.Value = v;
|
||||
}
|
||||
}
|
||||
|
||||
vm.PropertyChanged += Parameter_PropertyChanged;
|
||||
CurrentParameters.Add(vm);
|
||||
}
|
||||
|
||||
RecomputePreview();
|
||||
|
||||
// Notify so the host re-evaluates Save-button enablement on template selection — not only
|
||||
// on later parameter edits (the load/edit path does not raise SelectionChanged itself).
|
||||
OnPropertyChanged(nameof(IsAllValid));
|
||||
}
|
||||
|
||||
private void DetachParameterListeners()
|
||||
{
|
||||
foreach (var p in CurrentParameters)
|
||||
{
|
||||
p.PropertyChanged -= Parameter_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void Parameter_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(TemplateParameterViewModel.Value))
|
||||
{
|
||||
RecomputePreview();
|
||||
OnPropertyChanged(nameof(IsAllValid));
|
||||
}
|
||||
}
|
||||
|
||||
private void RecomputePreview()
|
||||
{
|
||||
if (_selectedTemplate is null)
|
||||
{
|
||||
ResolvedCommandLine = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
var resolved = TemplateResolver.Resolve(_selectedTemplate, CollectParameterValues());
|
||||
ResolvedCommandLine = string.IsNullOrEmpty(resolved.Args)
|
||||
? resolved.Executable
|
||||
: $"{resolved.Executable} {resolved.Args}";
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? prop = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 KeyboardManagerEditorUI.ViewModels
|
||||
{
|
||||
public sealed class TemplateChoiceViewModel
|
||||
{
|
||||
public TemplateChoiceViewModel(string value, string displayText)
|
||||
{
|
||||
Value = value;
|
||||
DisplayText = displayText;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public string DisplayText { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using KeyboardManagerEditorUI.Helpers;
|
||||
using KeyboardManagerEditorUI.Templates;
|
||||
|
||||
namespace KeyboardManagerEditorUI.ViewModels
|
||||
{
|
||||
public sealed class TemplateParameterViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private string _value = string.Empty;
|
||||
private TemplateChoiceViewModel? _selectedChoice;
|
||||
|
||||
public TemplateParameterViewModel(TemplateParameter definition)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(definition);
|
||||
|
||||
Name = definition.Name;
|
||||
Label = ResourceHelper.GetString(definition.LabelResourceKey);
|
||||
Type = definition.Type;
|
||||
Required = definition.Required;
|
||||
|
||||
if (definition.Choices is not null)
|
||||
{
|
||||
Choices = definition.Choices
|
||||
.Select(c => new TemplateChoiceViewModel(c.Value, ResourceHelper.GetString(c.DisplayResourceKey)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public string Type { get; }
|
||||
|
||||
public bool Required { get; }
|
||||
|
||||
public IReadOnlyList<TemplateChoiceViewModel>? Choices { get; }
|
||||
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (_value != value)
|
||||
{
|
||||
_value = value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TemplateChoiceViewModel? SelectedChoice
|
||||
{
|
||||
get => _selectedChoice;
|
||||
set
|
||||
{
|
||||
if (!ReferenceEquals(_selectedChoice, value))
|
||||
{
|
||||
_selectedChoice = value;
|
||||
Value = value?.Value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid => !Required || !string.IsNullOrEmpty(Value);
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? prop = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,12 @@ namespace KeyboardManagerConstants
|
||||
// Name of the property use to store openUri.
|
||||
inline const std::wstring ShortcutOpenURI = L"openUri";
|
||||
|
||||
// Name of the property used to store the CLI command template ID (optional, round-tripped through the editor).
|
||||
inline const std::wstring TemplateIdSettingName = L"templateId";
|
||||
|
||||
// Name of the property used to store CLI command template parameter values (optional, round-tripped through the editor).
|
||||
inline const std::wstring TemplateParametersSettingName = L"templateParameters";
|
||||
|
||||
// Name of the property use to store shortcutOperationType.
|
||||
inline const std::wstring ShortcutOperationType = L"operationType";
|
||||
|
||||
|
||||
@@ -10,6 +10,58 @@
|
||||
#include "RemapShortcut.h"
|
||||
#include "Helpers.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Reads optional CLI command-template metadata (templateId + templateParameters) from a settings
|
||||
// entry into the shortcut. Safe to call for legacy entries: missing/malformed metadata is ignored
|
||||
// so it can never discard the surrounding mapping.
|
||||
void ReadTemplateMetadata(const json::JsonObject& entryObj, Shortcut& shortcut)
|
||||
{
|
||||
if (entryObj.HasKey(KeyboardManagerConstants::TemplateIdSettingName))
|
||||
{
|
||||
shortcut.templateId = entryObj.GetNamedString(KeyboardManagerConstants::TemplateIdSettingName, L"");
|
||||
}
|
||||
|
||||
if (entryObj.HasKey(KeyboardManagerConstants::TemplateParametersSettingName))
|
||||
{
|
||||
// Type-check before reading: malformed (non-object) metadata must not discard the whole mapping.
|
||||
auto paramsValue = entryObj.GetNamedValue(KeyboardManagerConstants::TemplateParametersSettingName);
|
||||
if (paramsValue.ValueType() == json::JsonValueType::Object)
|
||||
{
|
||||
for (auto const& kv : paramsValue.GetObjectW())
|
||||
{
|
||||
if (kv.Value().ValueType() == json::JsonValueType::String)
|
||||
{
|
||||
shortcut.templateParameters.emplace(
|
||||
std::wstring(kv.Key()),
|
||||
std::wstring(kv.Value().GetString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Writes optional CLI command-template metadata to a settings entry — only emitted when set so
|
||||
// non-template mappings produce clean JSON.
|
||||
void WriteTemplateMetadata(json::JsonObject& keys, const Shortcut& shortcut)
|
||||
{
|
||||
if (!shortcut.templateId.empty())
|
||||
{
|
||||
keys.SetNamedValue(KeyboardManagerConstants::TemplateIdSettingName, json::value(shortcut.templateId));
|
||||
}
|
||||
|
||||
if (!shortcut.templateParameters.empty())
|
||||
{
|
||||
json::JsonObject paramsObj;
|
||||
for (auto const& [k, v] : shortcut.templateParameters)
|
||||
{
|
||||
paramsObj.SetNamedValue(k, json::JsonValue::CreateStringValue(v));
|
||||
}
|
||||
keys.SetNamedValue(KeyboardManagerConstants::TemplateParametersSettingName, paramsObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to clear the OS Level shortcut remapping table
|
||||
void MappingConfiguration::ClearOSLevelShortcuts()
|
||||
{
|
||||
@@ -258,6 +310,9 @@ bool MappingConfiguration::LoadAppSpecificShortcutRemaps(const json::JsonObject&
|
||||
tempShortcut.alreadyRunningAction = static_cast<Shortcut::ProgramAlreadyRunningAction>(runProgramAlreadyRunningAction);
|
||||
tempShortcut.startWindowType = static_cast<Shortcut::StartWindowType>(runProgramStartWindowType);
|
||||
|
||||
// Optional template metadata (preserved through round-trip; safe to omit on legacy entries).
|
||||
ReadTemplateMetadata(it.GetObjectW(), tempShortcut);
|
||||
|
||||
AddAppSpecificShortcut(targetApp.c_str(), originalShortcut, tempShortcut);
|
||||
}
|
||||
else if (operationType == 2)
|
||||
@@ -353,6 +408,9 @@ bool MappingConfiguration::LoadShortcutRemaps(const json::JsonObject& jsonData,
|
||||
tempShortcut.alreadyRunningAction = static_cast<Shortcut::ProgramAlreadyRunningAction>(runProgramAlreadyRunningAction);
|
||||
tempShortcut.startWindowType = static_cast<Shortcut::StartWindowType>(runProgramStartWindowType);
|
||||
|
||||
// Optional template metadata (preserved through round-trip; safe to omit on legacy entries).
|
||||
ReadTemplateMetadata(it.GetObjectW(), tempShortcut);
|
||||
|
||||
AddOSLevelShortcut(originalShortcut, tempShortcut);
|
||||
}
|
||||
else if (operationType == 2)
|
||||
@@ -525,6 +583,8 @@ bool MappingConfiguration::SaveSettingsToFile()
|
||||
keys.SetNamedValue(KeyboardManagerConstants::RunProgramArgsSettingName, json::value(targetShortcut.runProgramArgs));
|
||||
keys.SetNamedValue(KeyboardManagerConstants::RunProgramStartInDirSettingName, json::value(targetShortcut.runProgramStartInDir));
|
||||
|
||||
WriteTemplateMetadata(keys, targetShortcut);
|
||||
|
||||
// we need to add this dummy data for backwards compatibility
|
||||
keys.SetNamedValue(KeyboardManagerConstants::NewTextSettingName, json::value(L"*Unsupported*"));
|
||||
}
|
||||
@@ -590,6 +650,8 @@ bool MappingConfiguration::SaveSettingsToFile()
|
||||
keys.SetNamedValue(KeyboardManagerConstants::RunProgramArgsSettingName, json::value(targetShortcut.runProgramArgs));
|
||||
keys.SetNamedValue(KeyboardManagerConstants::RunProgramStartInDirSettingName, json::value(targetShortcut.runProgramStartInDir));
|
||||
|
||||
WriteTemplateMetadata(keys, targetShortcut);
|
||||
|
||||
// we need to add this dummy data for backwards compatibility
|
||||
keys.SetNamedValue(KeyboardManagerConstants::NewTextSettingName, json::value(L"*Unsupported*"));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ModifierKey.h"
|
||||
|
||||
#include <compare>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
namespace KeyboardManagerInput
|
||||
@@ -71,6 +72,12 @@ public:
|
||||
std::wstring runProgramStartInDir;
|
||||
std::wstring uriToOpen;
|
||||
|
||||
// Optional: when non-empty, indicates this mapping was created from a CLI command template.
|
||||
std::wstring templateId;
|
||||
|
||||
// Optional: parameter values captured at template save time. Lexicographically ordered for stable JSON output.
|
||||
std::map<std::wstring, std::wstring> templateParameters;
|
||||
|
||||
ProgramAlreadyRunningAction alreadyRunningAction = ProgramAlreadyRunningAction::ShowWindow;
|
||||
ElevationLevel elevationLevel = ElevationLevel::NonElevated;
|
||||
OperationType operationType = OperationType::RemapShortcut;
|
||||
|
||||
@@ -760,8 +760,8 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the monitor's power state via VCP 0xD6: On (0x01) wakes the display,
|
||||
/// Standby/Suspend/Off put it to sleep.
|
||||
/// Set power state for this monitor.
|
||||
/// Note: Setting any state other than "On" will turn off the display.
|
||||
/// </summary>
|
||||
public async Task SetPowerStateAsync(int powerState)
|
||||
{
|
||||
@@ -791,6 +791,18 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Command to set power state
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task SetPowerState(int? state)
|
||||
{
|
||||
if (state.HasValue)
|
||||
{
|
||||
await SetPowerStateAsync(state.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public int Contrast
|
||||
{
|
||||
get => _contrast;
|
||||
@@ -947,9 +959,11 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the selected state straight to the hardware. Selecting On (0x01) wakes a
|
||||
// sleeping monitor: DDC/CI stays reachable in Standby/Suspend/Off(DPM), so the
|
||||
// write turns the panel back on (Off(Hard)/0x05 may still need a physical wake).
|
||||
if (item.Value == PowerStateItem.PowerStateOn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SetPowerStateAsync(item.Value);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace PowerDisplay.ViewModels;
|
||||
/// </summary>
|
||||
public class PowerStateItem
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP power mode value representing On state
|
||||
/// </summary>
|
||||
public const int PowerStateOn = 0x01;
|
||||
|
||||
/// <summary>
|
||||
/// VCP value for this power state
|
||||
/// </summary>
|
||||
|
||||
@@ -19,6 +19,5 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
Google,
|
||||
AzureAIInference,
|
||||
Ollama,
|
||||
PhiSilica,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
"google" or "googleai" or "googlegemini" => AIServiceType.Google,
|
||||
"azureaiinference" or "azureinference" => AIServiceType.AzureAIInference,
|
||||
"ollama" => AIServiceType.Ollama,
|
||||
"phisilica" or "phi" or "philm" => AIServiceType.PhiSilica,
|
||||
_ => AIServiceType.Unknown,
|
||||
};
|
||||
}
|
||||
@@ -52,7 +51,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
AIServiceType.Google => "Google",
|
||||
AIServiceType.AzureAIInference => "AzureAIInference",
|
||||
AIServiceType.Ollama => "Ollama",
|
||||
AIServiceType.PhiSilica => "PhiSilica",
|
||||
AIServiceType.Unknown => string.Empty,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, "Unsupported AI service type."),
|
||||
};
|
||||
@@ -74,7 +72,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
AIServiceType.Google => "google",
|
||||
AIServiceType.AzureAIInference => "azureaiinference",
|
||||
AIServiceType.Ollama => "ollama",
|
||||
AIServiceType.PhiSilica => "phisilica",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,15 +118,6 @@ public static class AIServiceTypeRegistry
|
||||
PrivacyLabel = "AdvancedPaste_OpenAI_PrivacyLabel",
|
||||
PrivacyUri = new Uri("https://openai.com/privacy"),
|
||||
},
|
||||
[AIServiceType.PhiSilica] = new AIServiceTypeMetadata
|
||||
{
|
||||
ServiceType = AIServiceType.PhiSilica,
|
||||
DisplayName = "Phi Silica",
|
||||
IconPath = "ms-appx:///Assets/Settings/Icons/Models/WindowsML.svg",
|
||||
IsOnlineService = false,
|
||||
IsLocalModel = true,
|
||||
LegalDescription = "AdvancedPaste_LocalModel_LegalDescription",
|
||||
},
|
||||
[AIServiceType.Unknown] = new AIServiceTypeMetadata
|
||||
{
|
||||
ServiceType = AIServiceType.Unknown,
|
||||
|
||||
@@ -12,15 +12,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
|
||||
public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction
|
||||
{
|
||||
private HotkeySettings _shortcut = new();
|
||||
private HotkeySettings _coachingShortcut = new();
|
||||
private bool _isShown;
|
||||
private string _prompt = string.Empty;
|
||||
private string _systemPrompt = string.Empty;
|
||||
private string _coachingPrompt = string.Empty;
|
||||
private string _coachingSystemPrompt = string.Empty;
|
||||
private string _providerId = string.Empty;
|
||||
private string _coachingProviderId = string.Empty;
|
||||
private bool _coachingEnabled;
|
||||
private bool _hasConflict;
|
||||
private string _tooltip;
|
||||
|
||||
@@ -41,20 +33,6 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("coaching-shortcut")]
|
||||
public HotkeySettings CoachingShortcut
|
||||
{
|
||||
get => _coachingShortcut;
|
||||
set
|
||||
{
|
||||
if (_coachingShortcut != value)
|
||||
{
|
||||
_coachingShortcut = value ?? new();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("isShown")]
|
||||
public bool IsShown
|
||||
{
|
||||
@@ -62,55 +40,6 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
|
||||
set => Set(ref _isShown, value);
|
||||
}
|
||||
|
||||
[JsonPropertyName("prompt")]
|
||||
public string Prompt
|
||||
{
|
||||
get => _prompt;
|
||||
set => Set(ref _prompt, value ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonPropertyName("system-prompt")]
|
||||
public string SystemPrompt
|
||||
{
|
||||
get => _systemPrompt;
|
||||
set => Set(ref _systemPrompt, value ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonPropertyName("coaching-prompt")]
|
||||
public string CoachingPrompt
|
||||
{
|
||||
get => _coachingPrompt;
|
||||
set => Set(ref _coachingPrompt, value ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonPropertyName("coaching-system-prompt")]
|
||||
public string CoachingSystemPrompt
|
||||
{
|
||||
get => _coachingSystemPrompt;
|
||||
set => Set(ref _coachingSystemPrompt, value ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonPropertyName("provider-id")]
|
||||
public string ProviderId
|
||||
{
|
||||
get => _providerId;
|
||||
set => Set(ref _providerId, value ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonPropertyName("coaching-provider-id")]
|
||||
public string CoachingProviderId
|
||||
{
|
||||
get => _coachingProviderId;
|
||||
set => Set(ref _coachingProviderId, value ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonPropertyName("coaching-enabled")]
|
||||
public bool CoachingEnabled
|
||||
{
|
||||
get => _coachingEnabled;
|
||||
set => Set(ref _coachingEnabled, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasConflict
|
||||
{
|
||||
|
||||
@@ -11,14 +11,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
|
||||
public sealed class AdvancedPasteAdditionalActions
|
||||
{
|
||||
private AdvancedPasteAdditionalAction _imageToText = new();
|
||||
private AdvancedPasteAdditionalAction _fixSpellingAndGrammar = new();
|
||||
private AdvancedPastePasteAsFileAction _pasteAsFile = new();
|
||||
private AdvancedPasteTranscodeAction _transcode = new();
|
||||
|
||||
public static class PropertyNames
|
||||
{
|
||||
public const string ImageToText = "image-to-text";
|
||||
public const string FixSpellingAndGrammar = "fix-spelling-and-grammar";
|
||||
public const string PasteAsFile = "paste-as-file";
|
||||
public const string Transcode = "transcode";
|
||||
}
|
||||
@@ -30,13 +28,6 @@ public sealed class AdvancedPasteAdditionalActions
|
||||
init => _imageToText = value ?? new();
|
||||
}
|
||||
|
||||
[JsonPropertyName(PropertyNames.FixSpellingAndGrammar)]
|
||||
public AdvancedPasteAdditionalAction FixSpellingAndGrammar
|
||||
{
|
||||
get => _fixSpellingAndGrammar;
|
||||
init => _fixSpellingAndGrammar = value ?? new();
|
||||
}
|
||||
|
||||
[JsonPropertyName(PropertyNames.PasteAsFile)]
|
||||
public AdvancedPastePasteAsFileAction PasteAsFile
|
||||
{
|
||||
@@ -53,7 +44,7 @@ public sealed class AdvancedPasteAdditionalActions
|
||||
|
||||
public IEnumerable<IAdvancedPasteAction> GetAllActions()
|
||||
{
|
||||
return GetAllActionsRecursive([ImageToText, FixSpellingAndGrammar, PasteAsFile, Transcode]);
|
||||
return GetAllActionsRecursive([ImageToText, PasteAsFile, Transcode]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -16,7 +16,6 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
|
||||
private string _name = string.Empty;
|
||||
private string _description = string.Empty;
|
||||
private string _prompt = string.Empty;
|
||||
private string _providerId = string.Empty;
|
||||
private HotkeySettings _shortcut = new();
|
||||
private bool _isShown;
|
||||
private bool _canMoveUp;
|
||||
@@ -65,13 +64,6 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("provider-id")]
|
||||
public string ProviderId
|
||||
{
|
||||
get => _providerId;
|
||||
set => Set(ref _providerId, value ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonPropertyName("shortcut")]
|
||||
public HotkeySettings Shortcut
|
||||
{
|
||||
@@ -146,7 +138,6 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
|
||||
Name = other.Name;
|
||||
Description = other.Description;
|
||||
Prompt = other.Prompt;
|
||||
ProviderId = other.ProviderId;
|
||||
Shortcut = other.GetShortcutClone();
|
||||
IsShown = other.IsShown;
|
||||
CanMoveUp = other.CanMoveUp;
|
||||
|
||||
@@ -1,20 +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.PowerToys.Settings.UI.Library;
|
||||
|
||||
/// <summary>
|
||||
/// Shared default prompts for built-in AI actions. Referenced by both the AdvancedPaste module
|
||||
/// and the Settings UI to ensure consistent defaults and enable "reset to default" functionality.
|
||||
/// </summary>
|
||||
public static class AdvancedPasteDefaultPrompts
|
||||
{
|
||||
public const string FixSpellingAndGrammar = "Fix all spelling and grammar errors in the following text. Return only the corrected text without any additional explanation or commentary.";
|
||||
|
||||
public const string FixSpellingAndGrammarSystem = "You are a professional proofreader. You fix spelling and grammar errors in text. You return only the corrected text with no commentary.";
|
||||
|
||||
public const string FixSpellingAndGrammarCoaching = "Briefly explain what was changed and why in terms of language rules. Be concise as reviewer.";
|
||||
|
||||
public const string FixSpellingAndGrammarCoachingSystem = "You are a writing coach and language teacher. You will be given an original sentence and a corrected version.";
|
||||
}
|
||||
@@ -68,7 +68,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
string[] additionalActionHeaderKeys =
|
||||
[
|
||||
"ImageToText",
|
||||
"FixSpellingAndGrammar",
|
||||
"PasteAsTxtFile",
|
||||
"PasteAsPngFile",
|
||||
"PasteAsHtmlFile",
|
||||
@@ -80,25 +79,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
if (action is AdvancedPasteAdditionalAction additionalAction)
|
||||
{
|
||||
var headerKey = additionalActionHeaderKeys[Math.Min(index, additionalActionHeaderKeys.Length - 1)];
|
||||
hotkeyAccessors.Add(new HotkeyAccessor(
|
||||
() => additionalAction.Shortcut,
|
||||
value => additionalAction.Shortcut = value ?? new HotkeySettings(),
|
||||
headerKey));
|
||||
additionalActionHeaderKeys[index]));
|
||||
index++;
|
||||
|
||||
// The coaching shortcut is registered by the runner as a separate hotkey
|
||||
// immediately after Fix Spelling and Grammar (and only when it's active), so it
|
||||
// must appear in the same position here to keep hotkey IDs aligned with conflicts.
|
||||
if (ReferenceEquals(additionalAction, Properties.AdditionalActions.FixSpellingAndGrammar)
|
||||
&& additionalAction.CoachingEnabled
|
||||
&& additionalAction.CoachingShortcut is { Code: not 0 })
|
||||
{
|
||||
hotkeyAccessors.Add(new HotkeyAccessor(
|
||||
() => additionalAction.CoachingShortcut,
|
||||
value => additionalAction.CoachingShortcut = value ?? new HotkeySettings(),
|
||||
"FixSpellingAndGrammarCoaching"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("operationType")]
|
||||
public int OperationType { get; set; }
|
||||
|
||||
[JsonPropertyName("templateId")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string TemplateId { get; set; }
|
||||
|
||||
[JsonPropertyName("templateParameters")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public Dictionary<string, string> TemplateParameters { get; set; }
|
||||
|
||||
private enum KeyboardManagerEditorType
|
||||
{
|
||||
KeyEditor = 0,
|
||||
|
||||
@@ -150,6 +150,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonSerializable(typeof(VcpCodeDisplayInfo))]
|
||||
[JsonSerializable(typeof(VcpValueInfo))]
|
||||
[JsonSerializable(typeof(List<string>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
[JsonSerializable(typeof(List<MonitorInfo>))]
|
||||
[JsonSerializable(typeof(List<VcpCodeDisplayInfo>))]
|
||||
[JsonSerializable(typeof(List<VcpValueInfo>))]
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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.Text.Json;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UnitTest.ModelsTests
|
||||
{
|
||||
[TestClass]
|
||||
public class KeysDataModelTemplateFieldsTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void TemplateFields_RoundTripThroughJson()
|
||||
{
|
||||
var original = new KeysDataModel
|
||||
{
|
||||
OriginalKeys = "162;67",
|
||||
NewRemapKeys = string.Empty,
|
||||
OperationType = 1,
|
||||
RunProgramFilePath = "%LOCALAPPDATA%\\PowerToys\\PowerToys.exe",
|
||||
RunProgramArgs = "--open-settings=ColorPicker",
|
||||
TemplateId = "settings.openModule",
|
||||
TemplateParameters = new Dictionary<string, string>
|
||||
{
|
||||
{ "module", "ColorPicker" },
|
||||
},
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
var decoded = JsonSerializer.Deserialize<KeysDataModel>(json);
|
||||
|
||||
Assert.AreEqual("settings.openModule", decoded.TemplateId);
|
||||
Assert.IsNotNull(decoded.TemplateParameters);
|
||||
Assert.AreEqual("ColorPicker", decoded.TemplateParameters["module"]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TemplateFields_OmittedFromJsonWhenNull()
|
||||
{
|
||||
var entry = new KeysDataModel
|
||||
{
|
||||
OriginalKeys = "162;67",
|
||||
NewRemapKeys = "162;86",
|
||||
OperationType = 0,
|
||||
TemplateId = null,
|
||||
TemplateParameters = null,
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(entry);
|
||||
|
||||
Assert.IsFalse(json.Contains("templateId"), "templateId should be omitted when null");
|
||||
Assert.IsFalse(json.Contains("templateParameters"), "templateParameters should be omitted when null");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TemplateFields_PresentInJsonWhenSet()
|
||||
{
|
||||
var entry = new KeysDataModel
|
||||
{
|
||||
OriginalKeys = "162;67",
|
||||
NewRemapKeys = string.Empty,
|
||||
OperationType = 1,
|
||||
RunProgramFilePath = "%LOCALAPPDATA%\\PowerToys\\PowerToys.exe",
|
||||
RunProgramArgs = "--open-settings",
|
||||
TemplateId = "settings.openMain",
|
||||
TemplateParameters = new Dictionary<string, string>(),
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(entry);
|
||||
|
||||
Assert.IsTrue(json.Contains("\"templateId\""), "templateId is non-null, should be serialized");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,17 +116,6 @@
|
||||
Header="{x:Bind ModelName, Mode=OneWay}"
|
||||
HeaderIcon="{x:Bind ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border
|
||||
Padding="6,2"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
||||
CornerRadius="4"
|
||||
Visibility="{x:Bind IsActive, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Uid="AdvancedPaste_DefaultBadge"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
|
||||
</Border>
|
||||
<Button
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
@@ -135,11 +124,6 @@
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="AdvancedPaste_SetAsDefault"
|
||||
Click="SetAsDefaultProviderButton_Click"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="{x:Bind}" />
|
||||
<MenuFlyoutItem
|
||||
x:Uid="AdvancedPaste_Edit"
|
||||
Click="EditPasteAIProviderButton_Click"
|
||||
@@ -180,7 +164,7 @@
|
||||
</InfoBar.IconSource>
|
||||
</InfoBar>
|
||||
</tkcontrols:SettingsExpander.ItemsHeader>
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.AdvancedPasteUIShortcut, Mode=TwoWay}" />
|
||||
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.AdvancedPasteUIShortcut, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard Name="AdvancedPasteEnableClipboardPreview" ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="AdvancedPaste_EnableClipboardPreview" IsChecked="{x:Bind ViewModel.EnableClipboardPreview, Mode=TwoWay}" />
|
||||
@@ -211,7 +195,7 @@
|
||||
Name="PasteAsPlainTextShortcut"
|
||||
x:Uid="PasteAsPlainText_Shortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.PasteAsPlainTextShortcut, Mode=TwoWay}" />
|
||||
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.PasteAsPlainTextShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
Name="PasteAsMarkdownShortcut"
|
||||
@@ -238,109 +222,6 @@
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Name="FixSpellingAndGrammar"
|
||||
x:Uid="FixSpellingAndGrammar"
|
||||
DataContext="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar, Mode=OneWay}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}"
|
||||
IsExpanded="{Binding IsShown, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander.Content>
|
||||
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
|
||||
</tkcontrols:SettingsExpander.Content>
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard Visibility="Collapsed" />
|
||||
<tkcontrols:SettingsCard x:Uid="AdvancedPaste_ActionProvider" IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.IsShown, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<ComboBox
|
||||
x:Name="FixSpellingProviderComboBox"
|
||||
MinWidth="200"
|
||||
DisplayMemberPath="DisplayName"
|
||||
ItemsSource="{x:Bind ViewModel.PasteAIConfiguration.Providers, Mode=OneWay}"
|
||||
PlaceholderText="{x:Bind GetDefaultProviderLabel()}"
|
||||
SelectedValue="{Binding ProviderId, Mode=TwoWay}"
|
||||
SelectedValuePath="Id" />
|
||||
<Button
|
||||
x:Uid="AdvancedPaste_ClearProviderSelection"
|
||||
VerticalAlignment="Center"
|
||||
Click="ClearProviderSelection_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=12}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="{x:Bind FixSpellingProviderComboBox}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="FixSpellingAndGrammar_CustomPrompt"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.IsShown, Mode=OneWay}">
|
||||
<TextBox PlaceholderText="{x:Bind models:AdvancedPasteDefaultPrompts.FixSpellingAndGrammar}" Text="{Binding Prompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="FixSpellingAndGrammar_SystemPrompt"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.IsShown, Mode=OneWay}">
|
||||
<TextBox PlaceholderText="{x:Bind models:AdvancedPasteDefaultPrompts.FixSpellingAndGrammarSystem}" Text="{Binding SystemPrompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<!-- Coaching mode -->
|
||||
<tkcontrols:SettingsCard x:Uid="FixSpellingAndGrammar_CoachingSection" IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.IsShown, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingShortcut, Mode=TwoWay}" />
|
||||
<ToggleSwitch
|
||||
IsOn="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=TwoWay}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="FixSpellingAndGrammar_CoachingProvider"
|
||||
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<ComboBox
|
||||
x:Name="CoachingProviderComboBox"
|
||||
MinWidth="200"
|
||||
DisplayMemberPath="DisplayName"
|
||||
ItemsSource="{x:Bind ViewModel.PasteAIConfiguration.Providers, Mode=OneWay}"
|
||||
PlaceholderText="{x:Bind GetDefaultProviderLabel()}"
|
||||
SelectedValue="{Binding CoachingProviderId, Mode=TwoWay}"
|
||||
SelectedValuePath="Id" />
|
||||
<Button
|
||||
x:Uid="AdvancedPaste_ClearProviderSelection"
|
||||
VerticalAlignment="Center"
|
||||
Click="ClearProviderSelection_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=12}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="{x:Bind CoachingProviderComboBox}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="FixSpellingAndGrammar_CoachingPrompt"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}">
|
||||
<TextBox PlaceholderText="{x:Bind models:AdvancedPasteDefaultPrompts.FixSpellingAndGrammarCoaching}" Text="{Binding CoachingPrompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="FixSpellingAndGrammar_CoachingSystemPrompt"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
IsEnabled="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.AdditionalActions.FixSpellingAndGrammar.CoachingEnabled, Mode=OneWay}">
|
||||
<TextBox PlaceholderText="{x:Bind models:AdvancedPasteDefaultPrompts.FixSpellingAndGrammarCoachingSystem}" Text="{Binding CoachingSystemPrompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Visibility="Collapsed" />
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="PasteAsFile"
|
||||
x:Uid="PasteAsFile"
|
||||
@@ -425,7 +306,7 @@
|
||||
<!-- Custom actions -->
|
||||
<controls:SettingsGroup x:Uid="AdvancedPaste_Additional_Actions_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Name="AdvancedPasteUIActions"
|
||||
Name="AdvancedPasteUIActions"
|
||||
x:Uid="AdvancedPasteUI_Actions"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}"
|
||||
@@ -544,28 +425,6 @@
|
||||
AcceptsReturn="true"
|
||||
Text="{Binding Prompt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextWrapping="Wrap" />
|
||||
<StackPanel
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal"
|
||||
Spacing="4">
|
||||
<ComboBox
|
||||
x:Name="CustomActionProviderComboBox"
|
||||
x:Uid="AdvancedPaste_CustomAction_Provider"
|
||||
Width="306"
|
||||
DisplayMemberPath="DisplayName"
|
||||
ItemsSource="{x:Bind ViewModel.PasteAIConfiguration.Providers, Mode=OneWay}"
|
||||
PlaceholderText="{x:Bind GetDefaultProviderLabel()}"
|
||||
SelectedValue="{Binding ProviderId, Mode=TwoWay}"
|
||||
SelectedValuePath="Id" />
|
||||
<Button
|
||||
x:Uid="AdvancedPaste_ClearProviderSelection"
|
||||
VerticalAlignment="Bottom"
|
||||
Click="ClearProviderSelection_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=12}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="{x:Bind CustomActionProviderComboBox}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
|
||||
@@ -683,92 +542,6 @@
|
||||
<controls:FoundryLocalModelPicker x:Name="FoundryLocalPicker" />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
x:Name="PhiSilicaPanel"
|
||||
Margin="0,8,0,0"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel
|
||||
x:Name="PhiSilicaLoadingPanel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="12">
|
||||
<ProgressRing
|
||||
Width="36"
|
||||
Height="36"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock
|
||||
x:Name="PhiSilicaLoadingText"
|
||||
x:Uid="AdvancedPaste_PhiSilicaLoadingStatus"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="PhiSilicaAvailablePanel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon
|
||||
HorizontalAlignment="Center"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
FontSize="24"
|
||||
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
x:Name="PhiSilicaAvailableText"
|
||||
HorizontalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="PhiSilicaNotAvailablePanel"
|
||||
Margin="48,0,48,48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8"
|
||||
Visibility="Collapsed">
|
||||
<Image Width="36" Source="ms-appx:///Assets/Settings/Icons/Models/WindowsML.svg" />
|
||||
<TextBlock
|
||||
x:Name="PhiSilicaNotAvailableTitle"
|
||||
HorizontalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
x:Name="PhiSilicaNotAvailableDescription"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
x:Name="PhiSilicaNotAvailableDetails"
|
||||
HorizontalAlignment="Center"
|
||||
FontFamily="Consolas"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||
IsTextSelectionEnabled="True"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="Collapsed" />
|
||||
<Button
|
||||
x:Name="PhiSilicaPrepareButton"
|
||||
x:Uid="AdvancedPaste_PhiSilicaPrepareButton"
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Click="PhiSilicaPrepareButton_Click"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
Visibility="Collapsed" />
|
||||
<HyperlinkButton
|
||||
x:Uid="AdvancedPaste_PhiSilicaCopilotLearnMore"
|
||||
HorizontalAlignment="Center"
|
||||
NavigateUri="https://learn.microsoft.com/windows/ai/npu-devices/" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel
|
||||
x:Name="PasteAIModelPanel"
|
||||
Orientation="Horizontal"
|
||||
|
||||
@@ -6,8 +6,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
@@ -32,7 +30,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
private CancellationTokenSource _foundryModelLoadCts;
|
||||
private bool _suppressFoundrySelectionChanged;
|
||||
private bool _isFoundryLocalAvailable;
|
||||
private bool _isPhiSilicaAvailable;
|
||||
private bool _disposed;
|
||||
private const string PasteAiDialogDefaultTitle = "Paste with AI provider configuration";
|
||||
|
||||
@@ -40,7 +37,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
private const string SimpleAISystemPrompt = "You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it. Do not output anything else besides the reformatted clipboard content.";
|
||||
private static readonly string AdvancedAISystemPromptNormalized = AdvancedAISystemPrompt.Trim();
|
||||
private static readonly string SimpleAISystemPromptNormalized = SimpleAISystemPrompt.Trim();
|
||||
private static readonly char[] NewLineSeparators = ['\r', '\n'];
|
||||
|
||||
private AdvancedPasteViewModel ViewModel { get; set; }
|
||||
|
||||
@@ -69,7 +65,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
ViewModel.OnPageLoaded();
|
||||
UpdatePasteAIUIVisibility();
|
||||
await UpdateFoundryLocalUIAsync();
|
||||
await UpdatePhiSilicaUIAsync();
|
||||
};
|
||||
|
||||
Unloaded += (_, _) =>
|
||||
@@ -88,7 +83,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
ViewModel.RefreshEnabledState();
|
||||
UpdatePasteAIUIVisibility();
|
||||
_ = UpdateFoundryLocalUIAsync();
|
||||
_ = UpdatePhiSilicaUIAsync();
|
||||
}
|
||||
|
||||
private void EnableAdvancedPasteAI() => ViewModel.EnableAI();
|
||||
@@ -109,8 +103,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
else
|
||||
{
|
||||
ViewModel.DisableAI();
|
||||
FixSpellingAndGrammar.IsExpanded = false;
|
||||
AdvancedPasteUIActions.IsExpanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,9 +319,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
bool requiresApiVersion = serviceKind == AIServiceType.AzureOpenAI;
|
||||
bool requiresModelPath = serviceKind == AIServiceType.Onnx;
|
||||
bool isFoundryLocal = serviceKind == AIServiceType.FoundryLocal;
|
||||
bool isPhiSilica = serviceKind == AIServiceType.PhiSilica;
|
||||
bool requiresApiKey = RequiresApiKeyForService(selectedType);
|
||||
bool requiresModelName = !isFoundryLocal && !isPhiSilica;
|
||||
bool showModerationToggle = serviceKind == AIServiceType.OpenAI;
|
||||
bool showAdvancedAI = serviceKind == AIServiceType.OpenAI || serviceKind == AIServiceType.AzureOpenAI;
|
||||
|
||||
@@ -354,7 +344,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
PasteAIModerationToggle.Visibility = showModerationToggle ? Visibility.Visible : Visibility.Collapsed;
|
||||
PasteAIEnableAdvancedAICheckBox.Visibility = showAdvancedAI ? Visibility.Visible : Visibility.Collapsed;
|
||||
PasteAIApiKeyPasswordBox.Visibility = requiresApiKey ? Visibility.Visible : Visibility.Collapsed;
|
||||
PasteAIModelNameTextBox.Visibility = requiresModelName ? Visibility.Visible : Visibility.Collapsed;
|
||||
PasteAIModelNameTextBox.Visibility = isFoundryLocal ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
if (requiresApiKey)
|
||||
{
|
||||
@@ -383,11 +373,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
// For Foundry Local, UpdateFoundrySaveButtonState will handle button state
|
||||
// based on model selection status
|
||||
}
|
||||
else if (isPhiSilica)
|
||||
{
|
||||
// For Phi Silica, UpdatePhiSilicaUIAsync will handle button state
|
||||
// based on device availability
|
||||
}
|
||||
else
|
||||
{
|
||||
// GPO allows this provider, enable save button
|
||||
@@ -436,360 +421,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task UpdatePhiSilicaUIAsync()
|
||||
{
|
||||
string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
|
||||
bool isPhiSilica = string.Equals(selectedType, "PhiSilica", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (PhiSilicaPanel is not null)
|
||||
{
|
||||
PhiSilicaPanel.Visibility = isPhiSilica ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (!isPhiSilica)
|
||||
{
|
||||
_isPhiSilicaAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (PasteAIProviderConfigurationDialog is not null)
|
||||
{
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
|
||||
}
|
||||
|
||||
ShowPhiSilicaLoadingState();
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
|
||||
try
|
||||
{
|
||||
// Settings doesn't have package identity, so it can't call
|
||||
// LanguageModel.GetReadyState() directly. Instead, probe via AdvancedPaste
|
||||
// which runs with its own package identity. See microsoft-ui-xaml#10856.
|
||||
var (status, diagnostics) = await Task.Run(() => CheckPhiSilicaViaAdvancedPaste());
|
||||
|
||||
if (status == "NotSupported")
|
||||
{
|
||||
_isPhiSilicaAvailable = false;
|
||||
ShowPhiSilicaNotAvailableState(
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Title"),
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Description"),
|
||||
details: diagnostics);
|
||||
}
|
||||
else if (status == "NotReady")
|
||||
{
|
||||
_isPhiSilicaAvailable = false;
|
||||
ShowPhiSilicaNotAvailableState(
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotReady_Title"),
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotReady_Description"),
|
||||
showPrepareButton: true,
|
||||
details: diagnostics);
|
||||
}
|
||||
else
|
||||
{
|
||||
_isPhiSilicaAvailable = true;
|
||||
ShowPhiSilicaAvailableState(resourceLoader.GetString("AdvancedPaste_PhiSilicaAvailable_Message"));
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_isPhiSilicaAvailable = false;
|
||||
ShowPhiSilicaNotAvailableState(
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Title"),
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaCheckFailed_Description"));
|
||||
}
|
||||
|
||||
if (PasteAIProviderConfigurationDialog is not null)
|
||||
{
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = _isPhiSilicaAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPhiSilicaLoadingState(string message = null)
|
||||
{
|
||||
if (PhiSilicaLoadingText is not null && !string.IsNullOrEmpty(message))
|
||||
{
|
||||
PhiSilicaLoadingText.Text = message;
|
||||
}
|
||||
|
||||
if (PhiSilicaLoadingPanel is not null)
|
||||
{
|
||||
PhiSilicaLoadingPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
if (PhiSilicaAvailablePanel is not null)
|
||||
{
|
||||
PhiSilicaAvailablePanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (PhiSilicaNotAvailablePanel is not null)
|
||||
{
|
||||
PhiSilicaNotAvailablePanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPhiSilicaAvailableState(string message)
|
||||
{
|
||||
if (PhiSilicaLoadingPanel is not null)
|
||||
{
|
||||
PhiSilicaLoadingPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (PhiSilicaAvailablePanel is not null)
|
||||
{
|
||||
PhiSilicaAvailablePanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
if (PhiSilicaAvailableText is not null)
|
||||
{
|
||||
PhiSilicaAvailableText.Text = message;
|
||||
}
|
||||
|
||||
if (PhiSilicaNotAvailablePanel is not null)
|
||||
{
|
||||
PhiSilicaNotAvailablePanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPhiSilicaNotAvailableState(string title, string description, bool showPrepareButton = false, string details = null)
|
||||
{
|
||||
if (PhiSilicaLoadingPanel is not null)
|
||||
{
|
||||
PhiSilicaLoadingPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (PhiSilicaAvailablePanel is not null)
|
||||
{
|
||||
PhiSilicaAvailablePanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (PhiSilicaNotAvailablePanel is not null)
|
||||
{
|
||||
PhiSilicaNotAvailablePanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
if (PhiSilicaNotAvailableTitle is not null)
|
||||
{
|
||||
PhiSilicaNotAvailableTitle.Text = title;
|
||||
}
|
||||
|
||||
if (PhiSilicaNotAvailableDescription is not null)
|
||||
{
|
||||
PhiSilicaNotAvailableDescription.Text = description;
|
||||
}
|
||||
|
||||
if (PhiSilicaPrepareButton is not null)
|
||||
{
|
||||
PhiSilicaPrepareButton.Visibility = showPrepareButton ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// Surface the AdvancedPaste diagnostics (LAF status, ready state, HRESULT) so the user
|
||||
// can see why the model isn't ready / why a download attempt failed, instead of nothing.
|
||||
if (PhiSilicaNotAvailableDetails is not null)
|
||||
{
|
||||
PhiSilicaNotAvailableDetails.Text = details ?? string.Empty;
|
||||
PhiSilicaNotAvailableDetails.Visibility = string.IsNullOrWhiteSpace(details) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks Phi Silica availability by launching AdvancedPaste.exe with --check-phi-silica.
|
||||
/// AdvancedPaste has sparse package identity and can call the Windows AI APIs directly.
|
||||
/// Returns the status ("Available", "NotReady", or "NotSupported") plus any diagnostic lines
|
||||
/// AdvancedPaste wrote to stderr (LAF unlock status, ready state).
|
||||
/// </summary>
|
||||
private static (string Status, string Diagnostics) CheckPhiSilicaViaAdvancedPaste()
|
||||
{
|
||||
var settingsDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
// PowerToys.AdvancedPaste.exe ships in the same WinUI3Apps folder as PowerToys.Settings.exe
|
||||
// (see installer harvest and .vscode/launch.json), not in an "AdvancedPaste" subfolder.
|
||||
var advancedPastePath = Path.Combine(settingsDir, "PowerToys.AdvancedPaste.exe");
|
||||
|
||||
if (!File.Exists(advancedPastePath))
|
||||
{
|
||||
return ("NotSupported", string.Empty);
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = advancedPastePath,
|
||||
Arguments = "--check-phi-silica",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
return ("NotSupported", string.Empty);
|
||||
}
|
||||
|
||||
// Read stdout/stderr asynchronously so a stalled child can't block us before the timeout elapses.
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
if (!process.WaitForExit(10_000))
|
||||
{
|
||||
TryKillProcess(process);
|
||||
return ("NotSupported", string.Empty);
|
||||
}
|
||||
|
||||
var output = outputTask.GetAwaiter().GetResult().Trim();
|
||||
var diagnostics = ExtractPhiSilicaDiagnostics(errorTask.GetAwaiter().GetResult());
|
||||
|
||||
var status = output switch
|
||||
{
|
||||
"Available" => "Available",
|
||||
"NotReady" => "NotReady",
|
||||
_ => "NotSupported",
|
||||
};
|
||||
|
||||
return (status, diagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers Phi Silica model preparation (download) by launching AdvancedPaste.exe with
|
||||
/// --prepare-phi-silica. AdvancedPaste has sparse package identity and can call EnsureReadyAsync.
|
||||
/// Returns the status ("Ready", "Failed", or "NotSupported") plus any diagnostic lines
|
||||
/// AdvancedPaste wrote to stderr (ready state and, on failure, the EnsureReadyAsync HRESULT).
|
||||
/// </summary>
|
||||
private static (string Status, string Diagnostics) PreparePhiSilicaViaAdvancedPaste()
|
||||
{
|
||||
var settingsDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
var advancedPastePath = Path.Combine(settingsDir, "PowerToys.AdvancedPaste.exe");
|
||||
|
||||
if (!File.Exists(advancedPastePath))
|
||||
{
|
||||
return ("NotSupported", string.Empty);
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = advancedPastePath,
|
||||
Arguments = "--prepare-phi-silica",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
return ("NotSupported", string.Empty);
|
||||
}
|
||||
|
||||
// Read stdout/stderr asynchronously; model download can take a while, but a stalled child
|
||||
// must not block us indefinitely, so cap the wait and kill the process on timeout.
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
if (!process.WaitForExit(600_000))
|
||||
{
|
||||
TryKillProcess(process);
|
||||
return ("Failed", string.Empty);
|
||||
}
|
||||
|
||||
var output = outputTask.GetAwaiter().GetResult().Trim();
|
||||
var diagnostics = ExtractPhiSilicaDiagnostics(errorTask.GetAwaiter().GetResult());
|
||||
|
||||
var status = output switch
|
||||
{
|
||||
"Ready" => "Ready",
|
||||
"Failed" => "Failed",
|
||||
_ => "NotSupported",
|
||||
};
|
||||
|
||||
return (status, diagnostics);
|
||||
}
|
||||
|
||||
private static void TryKillProcess(Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// AdvancedPaste writes structured Phi Silica diagnostics to stderr, one per line prefixed with
|
||||
// "[phi-silica] " (LAF unlock status, ready state, and on failure the EnsureReadyAsync HRESULT
|
||||
// and message). Pull those lines out so the configurator can show the real error/HRESULT.
|
||||
private static string ExtractPhiSilicaDiagnostics(string standardError)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(standardError))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
const string prefix = "[phi-silica] ";
|
||||
var lines = standardError
|
||||
.Split(NewLineSeparators, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(line => line.Trim())
|
||||
.Where(line => line.StartsWith(prefix, StringComparison.Ordinal))
|
||||
.Select(line => line.Substring(prefix.Length))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
private async void PhiSilicaPrepareButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
|
||||
ShowPhiSilicaLoadingState(resourceLoader.GetString("AdvancedPaste_PhiSilicaPreparing_Status"));
|
||||
|
||||
if (PasteAIProviderConfigurationDialog is not null)
|
||||
{
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
|
||||
}
|
||||
|
||||
(string Status, string Diagnostics) prepareResult;
|
||||
try
|
||||
{
|
||||
prepareResult = await Task.Run(() => PreparePhiSilicaViaAdvancedPaste());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
prepareResult = ("Failed", ex.Message);
|
||||
}
|
||||
|
||||
if (prepareResult.Status == "Ready")
|
||||
{
|
||||
// Model is now ready; re-probe so the UI flips to the available state and Save enables.
|
||||
await UpdatePhiSilicaUIAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_isPhiSilicaAvailable = false;
|
||||
|
||||
if (prepareResult.Status == "NotSupported")
|
||||
{
|
||||
ShowPhiSilicaNotAvailableState(
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Title"),
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotAvailable_Description"),
|
||||
details: prepareResult.Diagnostics);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowPhiSilicaNotAvailableState(
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaNotReady_Title"),
|
||||
resourceLoader.GetString("AdvancedPaste_PhiSilicaPrepareFailed_Description"),
|
||||
showPrepareButton: true,
|
||||
details: prepareResult.Diagnostics);
|
||||
}
|
||||
|
||||
if (PasteAIProviderConfigurationDialog is not null)
|
||||
{
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadFoundryLocalModelsAsync()
|
||||
{
|
||||
if (FoundryLocalPanel is null)
|
||||
@@ -1204,7 +835,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
AIServiceType.Onnx => false,
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.FoundryLocal => false,
|
||||
AIServiceType.PhiSilica => false,
|
||||
AIServiceType.ML => false,
|
||||
_ => true,
|
||||
};
|
||||
@@ -1482,7 +1112,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
}
|
||||
|
||||
await UpdateFoundryLocalUIAsync();
|
||||
await UpdatePhiSilicaUIAsync();
|
||||
UpdatePasteAIUIVisibility();
|
||||
RefreshDialogBindings();
|
||||
|
||||
@@ -1512,7 +1141,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
UpdatePasteAIUIVisibility();
|
||||
await UpdateFoundryLocalUIAsync();
|
||||
await UpdatePhiSilicaUIAsync();
|
||||
RefreshDialogBindings();
|
||||
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
|
||||
await PasteAIProviderConfigurationDialog.ShowAsync();
|
||||
@@ -1529,41 +1157,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
ViewModel?.RemovePasteAIProvider(provider);
|
||||
}
|
||||
|
||||
private void SetAsDefaultProviderButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not MenuFlyoutItem menuItem || menuItem.Tag is not PasteAIProviderDefinition provider)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel?.SetAsDefaultProvider(provider);
|
||||
}
|
||||
|
||||
private void PasteAIProviderConfigurationDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args)
|
||||
{
|
||||
ViewModel?.CancelPasteAIProviderDraft();
|
||||
PasteAIProviderConfigurationDialog.Title = PasteAiDialogDefaultTitle;
|
||||
PasteAIApiKeyPasswordBox.Password = string.Empty;
|
||||
}
|
||||
|
||||
private void ClearProviderSelection_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button && button.Tag is ComboBox comboBox)
|
||||
{
|
||||
comboBox.SelectedIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDefaultProviderLabel()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Microsoft.PowerToys.Settings.UI.Helpers.ResourceLoaderInstance.ResourceLoader.GetString("AdvancedPaste_ActionProvider_Default");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Default (use active provider)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1992,86 +1992,6 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
|
||||
<data name="ImageToText.Header" xml:space="preserve">
|
||||
<value>Image to text</value>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar.Header" xml:space="preserve">
|
||||
<value>Fix spelling and grammar</value>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CustomPrompt.Header" xml:space="preserve">
|
||||
<value>Custom prompt</value>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CustomPrompt.Description" xml:space="preserve">
|
||||
<value>Override the default AI prompt used for fixing spelling and grammar. Leave empty to use the default prompt.</value>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_SystemPrompt.Header" xml:space="preserve">
|
||||
<value>System prompt</value>
|
||||
<comment>Header for the system prompt customization for fix spelling action</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_SystemPrompt.Description" xml:space="preserve">
|
||||
<value>Override the system prompt that sets the AI's role for fixing spelling and grammar. Leave empty to use the default.</value>
|
||||
<comment>Description for the system prompt customization</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_ActionProvider.Header" xml:space="preserve">
|
||||
<value>AI model</value>
|
||||
<comment>Label for selecting which AI model provider to use for this action</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_ActionProvider.Description" xml:space="preserve">
|
||||
<value>Select the AI model to use for this action. Leave as default to use the globally active provider.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_ActionProvider_Default" xml:space="preserve">
|
||||
<value>Default (use active provider)</value>
|
||||
<comment>Option in provider picker that means use the globally configured provider</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_CustomAction_Provider.Header" xml:space="preserve">
|
||||
<value>AI model</value>
|
||||
<comment>Label for selecting the AI model provider in the custom action dialog</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_ClearProviderSelection.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Reset to default provider</value>
|
||||
<comment>Tooltip for button that clears the provider selection back to default</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_ClearProviderSelection.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Reset to default provider</value>
|
||||
<comment>Accessible name for button that clears the provider selection back to default</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingSection.Header" xml:space="preserve">
|
||||
<value>Coaching mode</value>
|
||||
<comment>Header for the coaching mode collapsible settings section</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingSection.Description" xml:space="preserve">
|
||||
<value>When enabled, shows a preview with an explanation of what was fixed and why.</value>
|
||||
<comment>Description for the coaching mode settings section</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingShortcut.Header" xml:space="preserve">
|
||||
<value>Coaching shortcut</value>
|
||||
<comment>Header for the coaching mode keyboard shortcut</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingShortcut.Description" xml:space="preserve">
|
||||
<value>Optional shortcut that triggers fix spelling with coaching enabled. If not set, uses the main action shortcut.</value>
|
||||
<comment>Description for the coaching shortcut setting</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingPrompt.Header" xml:space="preserve">
|
||||
<value>Coaching prompt</value>
|
||||
<comment>Header for the coaching prompt customization text box</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingProvider.Header" xml:space="preserve">
|
||||
<value>Coaching AI model</value>
|
||||
<comment>Label for selecting which AI model provider to use for coaching explanations</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingProvider.Description" xml:space="preserve">
|
||||
<value>Select the AI model to use for coaching explanations. Leave as default to use the same model as Fix Spelling.</value>
|
||||
<comment>Description for the coaching provider selector</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingPrompt.Description" xml:space="preserve">
|
||||
<value>Override the AI prompt used when generating the coaching explanation. Leave empty to use the default prompt.</value>
|
||||
<comment>Description for the coaching prompt customization</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingSystemPrompt.Header" xml:space="preserve">
|
||||
<value>Coaching system prompt</value>
|
||||
<comment>Header for the coaching system prompt customization text box</comment>
|
||||
</data>
|
||||
<data name="FixSpellingAndGrammar_CoachingSystemPrompt.Description" xml:space="preserve">
|
||||
<value>Override the system prompt that sets the AI's role for coaching explanations. Leave empty to use the default.</value>
|
||||
<comment>Description for the coaching system prompt customization</comment>
|
||||
</data>
|
||||
<data name="PasteAsFile.Header" xml:space="preserve">
|
||||
<value>Paste as file</value>
|
||||
</data>
|
||||
@@ -4471,11 +4391,11 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<comment>Enables display of clipboard contents preview in the Advanced Paste window</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_AutoCopySelectionForCustomActionHotkey.Header" xml:space="preserve">
|
||||
<value>Use selected text for all hotkeys</value>
|
||||
<value>Auto-copy selection for custom action hotkeys</value>
|
||||
<comment>Advanced Paste is a product name</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_AutoCopySelectionForCustomActionHotkey.Description" xml:space="preserve">
|
||||
<value>Attempts to copy the current text selection before running any Advanced Paste shortcut. Falls back to clipboard contents if nothing is selected.</value>
|
||||
<value>Attempts to copy the current selection before running a custom action shortcut</value>
|
||||
<comment>Advanced Paste is a product name</comment>
|
||||
</data>
|
||||
<data name="GPO_CommandNotFound_ForceDisabled.Title" xml:space="preserve">
|
||||
@@ -6043,14 +5963,6 @@ The break timer font matches the text font.</value>
|
||||
<data name="AdvancedPaste_Edit.Text" xml:space="preserve">
|
||||
<value>Edit</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_SetAsDefault.Text" xml:space="preserve">
|
||||
<value>Set as default</value>
|
||||
<comment>Menu item to mark a provider as the default/active one</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_DefaultBadge.Text" xml:space="preserve">
|
||||
<value>Default</value>
|
||||
<comment>Badge label shown on the provider that is currently the default/active one</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_Remove.Text" xml:space="preserve">
|
||||
<value>Remove</value>
|
||||
</data>
|
||||
@@ -6091,46 +6003,6 @@ The break timer font matches the text font.</value>
|
||||
<value>Foundry Local is still in public preview</value>
|
||||
<comment>Do not loc "Foundry Local"</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaLoadingStatus.Text" xml:space="preserve">
|
||||
<value>Checking Phi Silica availability...</value>
|
||||
<comment>Do not localize "Phi Silica", it's a model name</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaCopilotLearnMore.Content" xml:space="preserve">
|
||||
<value>Learn more about Copilot+ PCs</value>
|
||||
<comment>Do not localize "Copilot+ PCs", it's a product name</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaNotAvailable_Title" xml:space="preserve">
|
||||
<value>Phi Silica is not available on this device.</value>
|
||||
<comment>Do not localize "Phi Silica", it's a model name</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaNotAvailable_Description" xml:space="preserve">
|
||||
<value>A Copilot+ PC with an NPU is required to use Phi Silica. For on-device AI on any Windows PC, consider using Foundry Local.</value>
|
||||
<comment>Do not localize "Copilot+ PC", "NPU", "Phi Silica", and "Foundry Local", they are product/technical names</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaNotReady_Title" xml:space="preserve">
|
||||
<value>Phi Silica model is not ready.</value>
|
||||
<comment>Do not localize "Phi Silica", it's a model name</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaNotReady_Description" xml:space="preserve">
|
||||
<value>The model needs to be downloaded before it can be used. Select "Download model" to start, or check Windows Update for progress.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaPrepareButton.Content" xml:space="preserve">
|
||||
<value>Download model</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaPreparing_Status" xml:space="preserve">
|
||||
<value>Downloading and preparing the model. This may take several minutes...</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaPrepareFailed_Description" xml:space="preserve">
|
||||
<value>Couldn't prepare the model. Check Windows Update for the AI model download, then try again.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaAvailable_Message" xml:space="preserve">
|
||||
<value>Phi Silica is available and ready on this device.</value>
|
||||
<comment>Do not localize "Phi Silica", it's a model name</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_PhiSilicaCheckFailed_Description" xml:space="preserve">
|
||||
<value>Unable to check Phi Silica availability. A Copilot+ PC with an NPU is required.</value>
|
||||
<comment>Do not localize "Phi Silica", "Copilot+ PC", and "NPU", they are product/technical names</comment>
|
||||
</data>
|
||||
<data name="CmdPal_Settings.Description" xml:space="preserve">
|
||||
<value>Configure the activation shortcut, extensions, behavior and much more</value>
|
||||
</data>
|
||||
|
||||
@@ -139,15 +139,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
if (action is AdvancedPasteAdditionalAction additionalAction)
|
||||
{
|
||||
hotkeySettings.Add(additionalAction.Shortcut);
|
||||
|
||||
// Mirror the runner's hotkey order: the coaching shortcut is registered as a
|
||||
// separate hotkey immediately after Fix Spelling and Grammar when it's active.
|
||||
if (ReferenceEquals(additionalAction, _additionalActions.FixSpellingAndGrammar)
|
||||
&& additionalAction.CoachingEnabled
|
||||
&& additionalAction.CoachingShortcut is { Code: not 0 })
|
||||
{
|
||||
hotkeySettings.Add(additionalAction.CoachingShortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +492,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
var newValue = value ?? new PasteAIConfiguration();
|
||||
_advancedPasteSettings.Properties.PasteAIConfiguration = newValue;
|
||||
SyncProviderActiveFlags(newValue);
|
||||
SubscribeToPasteAIConfiguration(newValue);
|
||||
|
||||
OnPropertyChanged(nameof(PasteAIConfiguration));
|
||||
@@ -597,25 +587,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
|
||||
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
||||
|
||||
public bool IsAdditionalActionConflictingCopyShortcut
|
||||
{
|
||||
get
|
||||
{
|
||||
var shortcuts = _additionalActions.GetAllActions()
|
||||
.OfType<AdvancedPasteAdditionalAction>()
|
||||
.Select(additionalAction => additionalAction.Shortcut)
|
||||
.ToList();
|
||||
|
||||
// The coaching shortcut is a separately-registered hotkey; include it when active.
|
||||
var fixSpelling = _additionalActions.FixSpellingAndGrammar;
|
||||
if (fixSpelling.CoachingEnabled && fixSpelling.CoachingShortcut is { Code: not 0 })
|
||||
{
|
||||
shortcuts.Add(fixSpelling.CoachingShortcut);
|
||||
}
|
||||
|
||||
return shortcuts.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
||||
}
|
||||
}
|
||||
public bool IsAdditionalActionConflictingCopyShortcut =>
|
||||
_additionalActions.GetAllActions()
|
||||
.OfType<AdvancedPasteAdditionalAction>()
|
||||
.Select(additionalAction => additionalAction.Shortcut)
|
||||
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
||||
|
||||
private void NotifySettingsChanged()
|
||||
{
|
||||
@@ -805,25 +781,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAsDefaultProvider(PasteAIProviderDefinition provider)
|
||||
{
|
||||
if (provider is null || string.IsNullOrEmpty(provider.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var config = PasteAIConfiguration;
|
||||
if (config is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
config.ActiveProviderId = provider.Id;
|
||||
SyncProviderActiveFlags(config);
|
||||
SaveAndNotifySettings();
|
||||
OnPropertyChanged(nameof(PasteAIConfiguration));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
@@ -1126,9 +1083,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
SaveAndNotifySettings();
|
||||
|
||||
if (e.PropertyName is nameof(AdvancedPasteAdditionalAction.Shortcut)
|
||||
or nameof(AdvancedPasteAdditionalAction.CoachingShortcut)
|
||||
or nameof(AdvancedPasteAdditionalAction.CoachingEnabled))
|
||||
if (e.PropertyName == nameof(AdvancedPasteAdditionalAction.Shortcut))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsAdditionalActionConflictingCopyShortcut));
|
||||
}
|
||||
@@ -1369,7 +1324,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return true;
|
||||
}
|
||||
|
||||
if (existing?.ModerationEnabled != updated?.ModerationEnabled || existing?.EnableAdvancedAI != updated?.EnableAdvancedAI)
|
||||
if (existing?.ModerationEnabled != updated?.ModerationEnabled || existing?.EnableAdvancedAI != updated?.EnableAdvancedAI || existing?.IsActive != updated?.IsActive)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1456,12 +1411,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
if (sender is PasteAIProviderDefinition provider)
|
||||
{
|
||||
// IsActive is a UI-only (JsonIgnore) flag; don't save when it changes.
|
||||
if (string.Equals(e.PropertyName, nameof(PasteAIProviderDefinition.IsActive), StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// When service type changes we may need to update credentials entry names.
|
||||
if (string.Equals(e.PropertyName, nameof(PasteAIProviderDefinition.ServiceType), StringComparison.Ordinal))
|
||||
{
|
||||
@@ -1478,14 +1427,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.Providers), StringComparison.Ordinal))
|
||||
{
|
||||
SubscribeToPasteAIProviders(PasteAIConfiguration);
|
||||
SyncProviderActiveFlags(PasteAIConfiguration);
|
||||
SaveAndNotifySettings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal))
|
||||
{
|
||||
SyncProviderActiveFlags(PasteAIConfiguration);
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
}
|
||||
@@ -1501,32 +1448,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
|
||||
|
||||
SyncProviderActiveFlags(pasteConfig);
|
||||
SubscribeToPasteAIProviders(pasteConfig);
|
||||
}
|
||||
|
||||
private static void SyncProviderActiveFlags(PasteAIConfiguration config)
|
||||
{
|
||||
if (config?.Providers is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var activeId = config.ActiveProviderId;
|
||||
|
||||
// If no explicit active ID, default to the first provider
|
||||
if (string.IsNullOrEmpty(activeId) && config.Providers.Count > 0)
|
||||
{
|
||||
activeId = config.Providers[0].Id;
|
||||
config.ActiveProviderId = activeId;
|
||||
}
|
||||
|
||||
foreach (var provider in config.Providers)
|
||||
{
|
||||
provider.IsActive = !string.IsNullOrEmpty(activeId) && string.Equals(provider.Id, activeId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(credentialResource) || string.IsNullOrWhiteSpace(credentialUserName))
|
||||
|
||||
Reference in New Issue
Block a user