mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-07 11:00:32 +02:00
Compare commits
57 Commits
v0.98.0
...
copilot/cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53779a9803 | ||
|
|
fe31f79fde | ||
|
|
578df9b340 | ||
|
|
7cc4a16aa7 | ||
|
|
8c8c99c382 | ||
|
|
feae285c40 | ||
|
|
c34fb7f953 | ||
|
|
7d171a4428 | ||
|
|
2d037c4e91 | ||
|
|
0a69c93b87 | ||
|
|
a022a9f024 | ||
|
|
0b7d780980 | ||
|
|
7685cd1226 | ||
|
|
72bdfb073b | ||
|
|
96f97064be | ||
|
|
75a0fe1d2f | ||
|
|
5792d32d32 | ||
|
|
4cb3359314 | ||
|
|
943c2a1ff5 | ||
|
|
f686155d9b | ||
|
|
9afa1ec71d | ||
|
|
4337f8e5ff | ||
|
|
ed47bceac2 | ||
|
|
df23546c0b | ||
|
|
25f44bc6d9 | ||
|
|
dc533fbdb3 | ||
|
|
c05ba4e2c8 | ||
|
|
c83dd972a0 | ||
|
|
c33053b26b | ||
|
|
2cf7d0f5ec | ||
|
|
7cb0f3861a | ||
|
|
1106ac61f5 | ||
|
|
107bf3882c | ||
|
|
3f35b11cee | ||
|
|
1a9fcdcd1f | ||
|
|
6cf1d32e5a | ||
|
|
33497e59cc | ||
|
|
3d2f069c43 | ||
|
|
79d9b0e667 | ||
|
|
e2f611a7fc | ||
|
|
84ce86c573 | ||
|
|
735ea01a93 | ||
|
|
93f80f5f61 | ||
|
|
21f06b8bd0 | ||
|
|
fa78cc8ea7 | ||
|
|
cb9d54317a | ||
|
|
5d0eabed15 | ||
|
|
7051b8939b | ||
|
|
0d41d45a64 | ||
|
|
86115a54f6 | ||
|
|
99706d4324 | ||
|
|
ff194c0b5f | ||
|
|
a151d6c8b6 | ||
|
|
7610b77109 | ||
|
|
549b32e273 | ||
|
|
0ccf5986e9 | ||
|
|
87b24afa23 |
6
.github/actions/spell-check/allow/code.txt
vendored
6
.github/actions/spell-check/allow/code.txt
vendored
@@ -1,6 +1,7 @@
|
||||
# COLORS
|
||||
|
||||
argb
|
||||
Bgr
|
||||
bgra
|
||||
BLACKONWHITE
|
||||
BLUEGRAY
|
||||
@@ -28,6 +29,7 @@ RUS
|
||||
|
||||
AYUV
|
||||
bak
|
||||
HDP
|
||||
Bcl
|
||||
bgcode
|
||||
Deflatealgorithm
|
||||
@@ -46,6 +48,7 @@ nupkg
|
||||
petabyte
|
||||
resw
|
||||
resx
|
||||
runtimeconfig
|
||||
srt
|
||||
Stereolithography
|
||||
terabyte
|
||||
@@ -297,6 +300,8 @@ pwa
|
||||
|
||||
AOT
|
||||
Aot
|
||||
ify
|
||||
TFM
|
||||
|
||||
# YML
|
||||
onefuzz
|
||||
@@ -334,6 +339,7 @@ SETAUTOHIDEBAR
|
||||
WINDOWPOS
|
||||
WINEVENTPROC
|
||||
WORKERW
|
||||
FULLSCREENAPP
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
|
||||
1
.github/actions/spell-check/allow/names.txt
vendored
1
.github/actions/spell-check/allow/names.txt
vendored
@@ -223,6 +223,7 @@ Moq
|
||||
mozilla
|
||||
mspaint
|
||||
Newtonsoft
|
||||
NVIDIA
|
||||
onenote
|
||||
openai
|
||||
Quickime
|
||||
|
||||
124
.github/actions/spell-check/allow/zoomit.txt
vendored
124
.github/actions/spell-check/allow/zoomit.txt
vendored
@@ -1,9 +1,23 @@
|
||||
accelscroll
|
||||
acq
|
||||
adr
|
||||
Adr
|
||||
APPLYTOSUBMENUS
|
||||
AUDCLNT
|
||||
axisdefer
|
||||
axisflip
|
||||
axisstart
|
||||
bitmaps
|
||||
BREAKSCR
|
||||
BUFFERFLAGS
|
||||
Cands
|
||||
capturepath
|
||||
centiseconds
|
||||
CLASSW
|
||||
coeffs
|
||||
coprime
|
||||
CREATEDIBSECTION
|
||||
crossfades
|
||||
Ctl
|
||||
CTLCOLOR
|
||||
CTLCOLORBTN
|
||||
@@ -11,53 +25,163 @@ CTLCOLORDLG
|
||||
CTLCOLOREDIT
|
||||
CTLCOLORLISTBOX
|
||||
CTrim
|
||||
ddy
|
||||
DFCS
|
||||
dlg
|
||||
dlu
|
||||
DONTCARE
|
||||
downsample
|
||||
DRAWITEM
|
||||
DRAWITEMSTRUCT
|
||||
droppedband
|
||||
Droppedband
|
||||
dsum
|
||||
dupburst
|
||||
dupsegments
|
||||
DWLP
|
||||
EDITCONTROL
|
||||
ENABLEHOOK
|
||||
expectedlock
|
||||
fastscroll
|
||||
FDE
|
||||
GETCHANNELRECT
|
||||
GETCHECK
|
||||
GETSCREENSAVEACTIVE
|
||||
GETSCREENSAVETIMEOUT
|
||||
GETTHUMBRECT
|
||||
GIFs
|
||||
hcfdark
|
||||
hcfwhitespace
|
||||
HTBOTTOMRIGHT
|
||||
HTHEME
|
||||
htol
|
||||
ICONINFORMATION
|
||||
ICONWARNING
|
||||
Inj
|
||||
jumprecover
|
||||
KSDATAFORMAT
|
||||
latestcapture
|
||||
ldx
|
||||
LEFTNOWORDWRAP
|
||||
legitjumps
|
||||
letterbox
|
||||
lld
|
||||
llu
|
||||
llums
|
||||
logfont
|
||||
lookback
|
||||
lround
|
||||
lte
|
||||
luma
|
||||
Luma
|
||||
manualdrop
|
||||
maskcache
|
||||
maxstep
|
||||
MENUINFO
|
||||
mic
|
||||
middledrop
|
||||
middledrop
|
||||
MMRESULT
|
||||
momentumreversal
|
||||
mrate
|
||||
mrt
|
||||
narrowstrip
|
||||
ncapture
|
||||
ncm
|
||||
nduplicates
|
||||
niterations
|
||||
nmonitor
|
||||
NONCLIENTMETRICS
|
||||
nonvle
|
||||
nredraw
|
||||
nstop
|
||||
nsubpixel
|
||||
ntorn
|
||||
nvw
|
||||
osc
|
||||
OWNERDRAW
|
||||
PBGRA
|
||||
periodictrap
|
||||
pfdc
|
||||
playhead
|
||||
pointerreuse
|
||||
pwfx
|
||||
Qpc
|
||||
quantums
|
||||
RCZOOMITSCR
|
||||
realcapture
|
||||
REFKNOWNFOLDERID
|
||||
reposted
|
||||
SCREENSAVE
|
||||
SCRNSAVE
|
||||
SCRNSAVECONFIGURE
|
||||
scrnsavw
|
||||
Scrnsavw
|
||||
scrollramp
|
||||
SCROLLSIZEGRIP
|
||||
selftest
|
||||
SETBARCOLOR
|
||||
SETBKCOLOR
|
||||
SETDEFID
|
||||
SETRECT
|
||||
SETSCREENSAVETIMEOUT
|
||||
SHAREMODE
|
||||
SHAREVIOLATION
|
||||
shortlist
|
||||
slowthenfast
|
||||
smallstart
|
||||
SNIPOCR
|
||||
ssi
|
||||
startuprecovery
|
||||
stf
|
||||
stopafter
|
||||
STREAMFLAGS
|
||||
submix
|
||||
sxx
|
||||
sxy
|
||||
syy
|
||||
tallportal
|
||||
tci
|
||||
tcsicmp
|
||||
TEXTMETRIC
|
||||
tinystep
|
||||
tme
|
||||
toolbars
|
||||
TRACKMOUSEEVENT
|
||||
Unadvise
|
||||
vaddq
|
||||
vaddvq
|
||||
vandq
|
||||
vcgeq
|
||||
vdup
|
||||
vld
|
||||
vle
|
||||
Vle
|
||||
VLE
|
||||
vminq
|
||||
vmlal
|
||||
vmull
|
||||
vqaddq
|
||||
vshrn
|
||||
vsntprintf
|
||||
vsnwprintf
|
||||
vsync
|
||||
WASAPI
|
||||
WAVEFORMATEX
|
||||
WAVEFORMATEXTENSIBLE
|
||||
wfopen
|
||||
wideportal
|
||||
wil
|
||||
WMU
|
||||
wrapjump
|
||||
wtol
|
||||
WTSSESSION
|
||||
WTSUn
|
||||
XEnd
|
||||
XStart
|
||||
XStep
|
||||
YInternal
|
||||
ZMBS
|
||||
zncc
|
||||
Zncc
|
||||
ZNCC
|
||||
|
||||
9
.github/actions/spell-check/expect.txt
vendored
9
.github/actions/spell-check/expect.txt
vendored
@@ -528,6 +528,7 @@ GPOCA
|
||||
gpp
|
||||
gpu
|
||||
gradians
|
||||
GRGX
|
||||
GSM
|
||||
gtm
|
||||
guiddata
|
||||
@@ -715,7 +716,6 @@ jpnime
|
||||
Jsons
|
||||
jsonval
|
||||
jxr
|
||||
kbmcontrols
|
||||
keybd
|
||||
KEYBDDATA
|
||||
KEYBDINPUT
|
||||
@@ -862,7 +862,6 @@ Metacharacter
|
||||
metafile
|
||||
metapackage
|
||||
mfc
|
||||
mfalse
|
||||
Mgmt
|
||||
Microwaved
|
||||
midl
|
||||
@@ -924,7 +923,6 @@ mstsc
|
||||
msvcp
|
||||
MT
|
||||
MTND
|
||||
mtrue
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
muxc
|
||||
@@ -1048,6 +1046,8 @@ OEMCONVERT
|
||||
officehubintl
|
||||
OFN
|
||||
ofs
|
||||
OICI
|
||||
OICIIO
|
||||
oldcolor
|
||||
olditem
|
||||
oldpath
|
||||
@@ -1081,6 +1081,7 @@ Packagemanager
|
||||
PACL
|
||||
padx
|
||||
pady
|
||||
PAI
|
||||
PAINTSTRUCT
|
||||
PALETTEWINDOW
|
||||
PARENTNOTIFY
|
||||
@@ -1345,7 +1346,7 @@ SCREENFONTS
|
||||
screensaver
|
||||
screenshots
|
||||
scrollviewer
|
||||
SDDL
|
||||
sddl
|
||||
SDKDDK
|
||||
sdns
|
||||
searchterm
|
||||
|
||||
144
.github/scripts/generate-monaco-languages.js
vendored
Normal file
144
.github/scripts/generate-monaco-languages.js
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* generate-monaco-languages.js
|
||||
*
|
||||
* Headless replacement for the manual generateLanguagesJson.html workflow.
|
||||
* Serves the Monaco directory on a local HTTP server, then uses Playwright
|
||||
* to open generateLanguagesJson.html and capture the generated JSON.
|
||||
*
|
||||
* Monaco's AMD loader requires a real browser DOM, so we use Playwright
|
||||
* instead of trying to load it in Node.js directly.
|
||||
*
|
||||
* Usage: node generate-monaco-languages.js <path-to-src/Monaco>
|
||||
*
|
||||
* Prerequisites: npx playwright install chromium
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
|
||||
const monacoDir = process.argv[2];
|
||||
if (!monacoDir) {
|
||||
console.error("Usage: node generate-monaco-languages.js <monaco-dir>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const absMonacoDir = path.resolve(monacoDir);
|
||||
const outputPath = path.join(absMonacoDir, "monaco_languages.json");
|
||||
const htmlPath = path.join(absMonacoDir, "generateLanguagesJson.html");
|
||||
|
||||
if (!fs.existsSync(htmlPath)) {
|
||||
console.error(`generateLanguagesJson.html not found at: ${htmlPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// MIME types for serving Monaco files
|
||||
const MIME_TYPES = {
|
||||
".html": "text/html",
|
||||
".js": "application/javascript",
|
||||
".css": "text/css",
|
||||
".json": "application/json",
|
||||
".ttf": "font/ttf",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".svg": "image/svg+xml",
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a simple static file server rooted at absMonacoDir.
|
||||
*/
|
||||
function createServer() {
|
||||
return http.createServer((req, res) => {
|
||||
const url = new URL(req.url, "http://localhost");
|
||||
// Resolve the file path relative to absMonacoDir, preventing path traversal
|
||||
const requestedPath = decodeURIComponent(url.pathname);
|
||||
const filePath = path.resolve(absMonacoDir, requestedPath.replace(/^\/+/, ""));
|
||||
|
||||
// Ensure the resolved path is within absMonacoDir (case-insensitive on Windows)
|
||||
const normalizedFile = process.platform === "win32" ? filePath.toLowerCase() : filePath;
|
||||
const normalizedBase = process.platform === "win32" ? absMonacoDir.toLowerCase() : absMonacoDir;
|
||||
if (!normalizedFile.startsWith(normalizedBase + path.sep) && normalizedFile !== normalizedBase) {
|
||||
res.writeHead(403);
|
||||
res.end("Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
||||
|
||||
const content = fs.readFileSync(filePath);
|
||||
res.writeHead(200, { "Content-Type": contentType });
|
||||
res.end(content);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Start local HTTP server
|
||||
const server = createServer();
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = server.address().port;
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
console.log(`Local server started at ${baseUrl}`);
|
||||
|
||||
let browser;
|
||||
try {
|
||||
// Launch Playwright browser
|
||||
const { chromium } = require("playwright");
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
// Accept downloads so we can capture the generated file
|
||||
acceptDownloads: true,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
// The generateLanguagesJson.html auto-triggers a download of the JSON.
|
||||
// We intercept that download event to capture the content.
|
||||
const downloadPromise = page.waitForEvent("download", { timeout: 30000 });
|
||||
|
||||
console.log("Loading generateLanguagesJson.html in headless browser...");
|
||||
await page.goto(`${baseUrl}/generateLanguagesJson.html`, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Wait for the download to be triggered
|
||||
const download = await downloadPromise;
|
||||
console.log(`Download triggered: ${download.suggestedFilename()}`);
|
||||
|
||||
// Save the downloaded file
|
||||
const downloadPath = await download.path();
|
||||
const content = fs.readFileSync(downloadPath, "utf-8");
|
||||
|
||||
// Validate the JSON before writing
|
||||
const parsed = JSON.parse(content);
|
||||
if (!parsed.list || !Array.isArray(parsed.list) || parsed.list.length === 0) {
|
||||
throw new Error(
|
||||
"Generated JSON is invalid: missing or empty 'list' property"
|
||||
);
|
||||
}
|
||||
|
||||
// Write to output
|
||||
fs.writeFileSync(outputPath, content, "utf-8");
|
||||
console.log(
|
||||
`monaco_languages.json written with ${parsed.list.length} languages.`
|
||||
);
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Error:", err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
328
.github/scripts/tests/validate-monaco-update.tests.ps1
vendored
Normal file
328
.github/scripts/tests/validate-monaco-update.tests.ps1
vendored
Normal file
@@ -0,0 +1,328 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates that a Monaco Editor update was performed correctly.
|
||||
|
||||
.DESCRIPTION
|
||||
Runs a series of checks against the Monaco Editor files in the repository
|
||||
to ensure the update is valid and no regressions were introduced.
|
||||
|
||||
Tests:
|
||||
- loader.js exists and contains version info
|
||||
- monaco_languages.json is valid JSON with expected structure
|
||||
- All expected built-in Monaco languages are present
|
||||
- All PowerToys custom languages are registered
|
||||
- Custom language extension mappings are present
|
||||
- Monaco directory structure is intact
|
||||
- No empty/corrupt core files
|
||||
|
||||
.PARAMETER RepoRoot
|
||||
The root of the PowerToys repository. Defaults to the repo root
|
||||
relative to this script.
|
||||
|
||||
.EXAMPLE
|
||||
./validate-monaco-update.tests.ps1
|
||||
./validate-monaco-update.tests.ps1 -RepoRoot "C:\src\PowerToys"
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter()]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $RepoRoot) {
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot ".." ".." "..")).Path
|
||||
}
|
||||
|
||||
$monacoDir = Join-Path $RepoRoot "src" "Monaco"
|
||||
$monacoSrcDir = Join-Path $monacoDir "monacoSRC"
|
||||
$minDir = Join-Path $monacoSrcDir "min"
|
||||
$loaderJsPath = Join-Path $minDir "vs" "loader.js"
|
||||
$languagesJsonPath = Join-Path $monacoDir "monaco_languages.json"
|
||||
$specialLangsPath = Join-Path $monacoDir "monacoSpecialLanguages.js"
|
||||
$customLangsDir = Join-Path $monacoDir "customLanguages"
|
||||
|
||||
$testsPassed = 0
|
||||
$testsFailed = 0
|
||||
$testResults = @()
|
||||
|
||||
function Assert-Test {
|
||||
param(
|
||||
[string]$Name,
|
||||
[scriptblock]$Test
|
||||
)
|
||||
|
||||
try {
|
||||
$result = & $Test
|
||||
if ($result -eq $false) {
|
||||
throw "Assertion returned false"
|
||||
}
|
||||
$script:testsPassed++
|
||||
$script:testResults += [PSCustomObject]@{ Name = $Name; Status = "PASS"; Error = $null }
|
||||
Write-Host " [PASS] $Name" -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
$script:testsFailed++
|
||||
$script:testResults += [PSCustomObject]@{ Name = $Name; Status = "FAIL"; Error = $_.Exception.Message }
|
||||
Write-Host " [FAIL] $Name" -ForegroundColor Red
|
||||
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "=== Monaco Editor Update Validation ===" -ForegroundColor Cyan
|
||||
Write-Host "Repository root: $RepoRoot"
|
||||
Write-Host ""
|
||||
|
||||
# ─── Test Group 1: Directory Structure ────────────────────────────────
|
||||
Write-Host "--- Directory Structure ---" -ForegroundColor Yellow
|
||||
|
||||
Assert-Test "Monaco directory exists" {
|
||||
Test-Path $monacoDir
|
||||
}
|
||||
|
||||
Assert-Test "monacoSRC directory exists" {
|
||||
Test-Path $monacoSrcDir
|
||||
}
|
||||
|
||||
Assert-Test "min directory exists" {
|
||||
Test-Path $minDir
|
||||
}
|
||||
|
||||
Assert-Test "vs subdirectory exists" {
|
||||
Test-Path (Join-Path $minDir "vs")
|
||||
}
|
||||
|
||||
Assert-Test "editor directory exists" {
|
||||
Test-Path (Join-Path $minDir "vs" "editor")
|
||||
}
|
||||
|
||||
Assert-Test "basic-languages directory exists" {
|
||||
Test-Path (Join-Path $minDir "vs" "basic-languages")
|
||||
}
|
||||
|
||||
Assert-Test "base directory exists" {
|
||||
Test-Path (Join-Path $minDir "vs" "base")
|
||||
}
|
||||
|
||||
Assert-Test "language directory exists" {
|
||||
Test-Path (Join-Path $minDir "vs" "language")
|
||||
}
|
||||
|
||||
Assert-Test "customLanguages directory exists" {
|
||||
Test-Path $customLangsDir
|
||||
}
|
||||
|
||||
# ─── Test Group 2: Core Files ─────────────────────────────────────────
|
||||
Write-Host "`n--- Core Files ---" -ForegroundColor Yellow
|
||||
|
||||
Assert-Test "loader.js exists" {
|
||||
Test-Path $loaderJsPath
|
||||
}
|
||||
|
||||
Assert-Test "loader.js is not empty" {
|
||||
(Get-Item $loaderJsPath).Length -gt 0
|
||||
}
|
||||
|
||||
Assert-Test "loader.js contains version string" {
|
||||
$content = Get-Content $loaderJsPath -Raw
|
||||
$content -match 'Version:\s*\d+\.\d+\.\d+'
|
||||
}
|
||||
|
||||
Assert-Test "editor.main.js exists" {
|
||||
Test-Path (Join-Path $minDir "vs" "editor" "editor.main.js")
|
||||
}
|
||||
|
||||
Assert-Test "editor.main.js is not empty" {
|
||||
(Get-Item (Join-Path $minDir "vs" "editor" "editor.main.js")).Length -gt 0
|
||||
}
|
||||
|
||||
Assert-Test "editor.main.css exists" {
|
||||
Test-Path (Join-Path $minDir "vs" "editor" "editor.main.css")
|
||||
}
|
||||
|
||||
Assert-Test "monacoSpecialLanguages.js exists" {
|
||||
Test-Path $specialLangsPath
|
||||
}
|
||||
|
||||
Assert-Test "generateLanguagesJson.html exists" {
|
||||
Test-Path (Join-Path $monacoDir "generateLanguagesJson.html")
|
||||
}
|
||||
|
||||
Assert-Test "index.html exists" {
|
||||
Test-Path (Join-Path $monacoDir "index.html")
|
||||
}
|
||||
|
||||
Assert-Test "customTokenThemeRules.js exists" {
|
||||
Test-Path (Join-Path $monacoDir "customTokenThemeRules.js")
|
||||
}
|
||||
|
||||
# ─── Test Group 3: monaco_languages.json ──────────────────────────────
|
||||
Write-Host "`n--- monaco_languages.json ---" -ForegroundColor Yellow
|
||||
|
||||
Assert-Test "monaco_languages.json exists" {
|
||||
Test-Path $languagesJsonPath
|
||||
}
|
||||
|
||||
Assert-Test "monaco_languages.json is not empty" {
|
||||
(Get-Item $languagesJsonPath).Length -gt 0
|
||||
}
|
||||
|
||||
$languagesJson = $null
|
||||
Assert-Test "monaco_languages.json is valid JSON" {
|
||||
$script:languagesJson = Get-Content $languagesJsonPath -Raw | ConvertFrom-Json
|
||||
$null -ne $script:languagesJson
|
||||
}
|
||||
|
||||
Assert-Test "JSON has 'list' property" {
|
||||
$null -ne $languagesJson.list
|
||||
}
|
||||
|
||||
Assert-Test "Language list is a non-empty array" {
|
||||
$languagesJson.list.Count -gt 0
|
||||
}
|
||||
|
||||
# Minimum expected languages from built-in Monaco
|
||||
# These are a core subset that should always be present
|
||||
$expectedBuiltinLanguages = @(
|
||||
"plaintext", "javascript", "typescript", "html", "css", "json",
|
||||
"xml", "markdown", "yaml", "python", "java", "csharp", "cpp",
|
||||
"go", "rust", "ruby", "php", "sql", "shell", "powershell",
|
||||
"dockerfile", "bat", "fsharp", "lua", "r", "swift", "kotlin",
|
||||
"scala", "perl", "dart", "ini", "vb"
|
||||
)
|
||||
|
||||
Assert-Test "Minimum language count check (at least 80 languages)" {
|
||||
$languagesJson.list.Count -ge 80
|
||||
}
|
||||
|
||||
$languageIds = $languagesJson.list | ForEach-Object { $_.id }
|
||||
|
||||
foreach ($lang in $expectedBuiltinLanguages) {
|
||||
Assert-Test "Built-in language '$lang' is present" {
|
||||
$lang -in $languageIds
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Test Group 4: PowerToys Custom Languages ─────────────────────────
|
||||
Write-Host "`n--- PowerToys Custom Languages ---" -ForegroundColor Yellow
|
||||
|
||||
# Custom languages defined in monacoSpecialLanguages.js
|
||||
$expectedCustomLanguages = @(
|
||||
@{ Id = "reg"; Extensions = @(".reg") },
|
||||
@{ Id = "gitignore"; Extensions = @(".gitignore") },
|
||||
@{ Id = "srt"; Extensions = @(".srt") }
|
||||
)
|
||||
|
||||
foreach ($custom in $expectedCustomLanguages) {
|
||||
Assert-Test "Custom language '$($custom.Id)' is registered" {
|
||||
$custom.Id -in $languageIds
|
||||
}
|
||||
|
||||
foreach ($ext in $custom.Extensions) {
|
||||
Assert-Test "Custom language '$($custom.Id)' has extension '$ext'" {
|
||||
$lang = $languagesJson.list | Where-Object { $_.id -eq $custom.Id }
|
||||
if ($null -eq $lang) { throw "Language not found" }
|
||||
$ext -in $lang.extensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Custom language definition files exist
|
||||
$expectedCustomFiles = @("reg.js", "gitignore.js", "srt.js")
|
||||
foreach ($file in $expectedCustomFiles) {
|
||||
Assert-Test "Custom language file '$file' exists" {
|
||||
Test-Path (Join-Path $customLangsDir $file)
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Test Group 5: Custom Language Extensions ─────────────────────────
|
||||
Write-Host "`n--- Custom Language Extensions ---" -ForegroundColor Yellow
|
||||
|
||||
$expectedExtensions = @(
|
||||
@{ Id = "cppExt"; Extensions = @(".ino", ".pde") },
|
||||
@{ Id = "xmlExt"; Extensions = @(".wsdl", ".csproj", ".vcxproj", ".vbproj", ".fsproj", ".resx", ".resw") },
|
||||
@{ Id = "txtExt"; Extensions = @(".sln", ".log") },
|
||||
@{ Id = "razorExt"; Extensions = @(".razor") },
|
||||
@{ Id = "vbExt"; Extensions = @(".vbs") },
|
||||
@{ Id = "iniExt"; Extensions = @(".inf") },
|
||||
@{ Id = "shellExt"; Extensions = @(".ksh", ".zsh", ".bsh") }
|
||||
)
|
||||
|
||||
foreach ($ext in $expectedExtensions) {
|
||||
Assert-Test "Extension mapping '$($ext.Id)' is registered" {
|
||||
$ext.Id -in $languageIds
|
||||
}
|
||||
|
||||
# Spot-check at least one extension from each mapping
|
||||
$firstExt = $ext.Extensions[0]
|
||||
Assert-Test "Extension mapping '$($ext.Id)' has extension '$firstExt'" {
|
||||
$lang = $languagesJson.list | Where-Object { $_.id -eq $ext.Id }
|
||||
if ($null -eq $lang) { throw "Language not found" }
|
||||
$firstExt -in $lang.extensions
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Test Group 6: Language Entry Structure ───────────────────────────
|
||||
Write-Host "`n--- Language Entry Structure ---" -ForegroundColor Yellow
|
||||
|
||||
Assert-Test "Every language has an 'id' field" {
|
||||
$missing = @($languagesJson.list | Where-Object { -not $_.id -or $_.id.Trim() -eq "" })
|
||||
$missing.Count -eq 0
|
||||
}
|
||||
|
||||
Assert-Test "No duplicate language IDs" {
|
||||
$ids = $languagesJson.list | ForEach-Object { $_.id }
|
||||
$uniqueIds = $ids | Select-Object -Unique
|
||||
$ids.Count -eq $uniqueIds.Count
|
||||
}
|
||||
|
||||
Assert-Test "JSON language with extensions has array-type extensions" {
|
||||
$withExtensions = @($languagesJson.list | Where-Object {
|
||||
($_.PSObject.Properties.Name -contains "extensions") -and ($null -ne $_.extensions)
|
||||
})
|
||||
$invalid = @($withExtensions | Where-Object { $_.extensions -isnot [array] })
|
||||
$invalid.Count -eq 0
|
||||
}
|
||||
|
||||
# ─── Test Group 7: Version Consistency ────────────────────────────────
|
||||
Write-Host "`n--- Version Consistency ---" -ForegroundColor Yellow
|
||||
|
||||
Assert-Test "All NLS files in editor directory have matching versions" {
|
||||
$editorDir = Join-Path $minDir "vs" "editor"
|
||||
$nlsFiles = Get-ChildItem -Path $editorDir -Filter "editor.main.nls*.js" -ErrorAction SilentlyContinue
|
||||
if ($nlsFiles.Count -eq 0) { throw "No NLS files found" }
|
||||
|
||||
$loaderContent = Get-Content $loaderJsPath -Raw
|
||||
$null = $loaderContent -match 'Version:\s*(\d+\.\d+\.\d+)'
|
||||
$loaderVersion = $Matches[1]
|
||||
|
||||
foreach ($nlsFile in $nlsFiles) {
|
||||
$content = Get-Content $nlsFile.FullName -Raw -ErrorAction SilentlyContinue
|
||||
if ($content -and $content -match 'Version:\s*(\d+\.\d+\.\d+)') {
|
||||
if ($Matches[1] -ne $loaderVersion) {
|
||||
throw "Version mismatch in $($nlsFile.Name): expected $loaderVersion, found $($Matches[1])"
|
||||
}
|
||||
}
|
||||
}
|
||||
$true
|
||||
}
|
||||
|
||||
# ─── Summary ──────────────────────────────────────────────────────────
|
||||
Write-Host ""
|
||||
Write-Host "=== Test Summary ===" -ForegroundColor Cyan
|
||||
Write-Host "Passed: $testsPassed" -ForegroundColor Green
|
||||
Write-Host "Failed: $testsFailed" -ForegroundColor $(if ($testsFailed -gt 0) { "Red" } else { "Green" })
|
||||
Write-Host "Total: $($testsPassed + $testsFailed)"
|
||||
|
||||
if ($testsFailed -gt 0) {
|
||||
Write-Host "`nFailed tests:" -ForegroundColor Red
|
||||
$testResults | Where-Object { $_.Status -eq "FAIL" } | ForEach-Object {
|
||||
Write-Host " - $($_.Name): $($_.Error)" -ForegroundColor Red
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "`nAll tests passed!" -ForegroundColor Green
|
||||
exit 0
|
||||
150
.github/scripts/update-monaco-editor.ps1
vendored
Normal file
150
.github/scripts/update-monaco-editor.ps1
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Updates the Monaco Editor in PowerToys to the latest (or specified) version.
|
||||
|
||||
.DESCRIPTION
|
||||
This script automates the Monaco Editor update process described in
|
||||
doc/devdocs/common/FilePreviewCommon.md:
|
||||
1. Downloads Monaco editor via npm
|
||||
2. Copies the min folder into src/Monaco/monacoSRC/
|
||||
3. Generates the monaco_languages.json file using Node.js (headless)
|
||||
|
||||
.PARAMETER Version
|
||||
The Monaco Editor npm version to install. Defaults to "latest".
|
||||
|
||||
.PARAMETER RepoRoot
|
||||
The root of the PowerToys repository. Defaults to the repo root relative to this script.
|
||||
|
||||
.EXAMPLE
|
||||
./update-monaco-editor.ps1
|
||||
./update-monaco-editor.ps1 -Version "0.50.0"
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter()]
|
||||
[string]$Version = "latest",
|
||||
|
||||
[Parameter()]
|
||||
[string]$RepoRoot
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $RepoRoot) {
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot ".." "..")).Path
|
||||
}
|
||||
|
||||
$monacoDir = Join-Path $RepoRoot "src" "Monaco"
|
||||
$monacoSrcDir = Join-Path $monacoDir "monacoSRC"
|
||||
$languagesJsonPath = Join-Path $monacoDir "monaco_languages.json"
|
||||
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "monaco-update-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))"
|
||||
|
||||
Write-Host "=== Monaco Editor Update Script ==="
|
||||
Write-Host "Repository root: $RepoRoot"
|
||||
Write-Host "Target version: $Version"
|
||||
Write-Host "Temp directory: $tempDir"
|
||||
|
||||
# Verify prerequisites
|
||||
$npmPath = Get-Command npm -ErrorAction SilentlyContinue
|
||||
if (-not $npmPath) {
|
||||
throw "npm is required but not found in PATH. Please install Node.js."
|
||||
}
|
||||
|
||||
$nodePath = Get-Command node -ErrorAction SilentlyContinue
|
||||
if (-not $nodePath) {
|
||||
throw "node is required but not found in PATH. Please install Node.js."
|
||||
}
|
||||
|
||||
# Verify repo structure
|
||||
if (-not (Test-Path $monacoDir)) {
|
||||
throw "Monaco directory not found at: $monacoDir"
|
||||
}
|
||||
|
||||
try {
|
||||
# Step 1: Download Monaco via npm
|
||||
Write-Host "`n--- Step 1: Downloading Monaco Editor ($Version) via npm ---"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
Push-Location $tempDir
|
||||
try {
|
||||
$versionSpec = if ($Version -eq "latest") { "monaco-editor@latest" } else { "monaco-editor@$Version" }
|
||||
npm init -y 2>&1 | Out-Null
|
||||
npm install $versionSpec 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "npm install failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
$downloadedMinDir = Join-Path $tempDir "node_modules" "monaco-editor" "min"
|
||||
if (-not (Test-Path $downloadedMinDir)) {
|
||||
throw "Downloaded Monaco min directory not found at: $downloadedMinDir"
|
||||
}
|
||||
|
||||
# Detect the downloaded version from loader.js
|
||||
$loaderJsPath = Join-Path $downloadedMinDir "vs" "loader.js"
|
||||
if (-not (Test-Path $loaderJsPath)) {
|
||||
throw "loader.js not found in downloaded Monaco package"
|
||||
}
|
||||
|
||||
$loaderContent = Get-Content $loaderJsPath -Raw
|
||||
if ($loaderContent -match 'Version:\s*(\d+\.\d+\.\d+)') {
|
||||
$newVersion = $Matches[1]
|
||||
Write-Host "Downloaded Monaco version: $newVersion"
|
||||
}
|
||||
else {
|
||||
Write-Warning "Could not detect version from loader.js"
|
||||
$newVersion = $Version
|
||||
}
|
||||
|
||||
# Step 2: Replace monacoSRC/min folder
|
||||
Write-Host "`n--- Step 2: Replacing monacoSRC with new version ---"
|
||||
$targetMinDir = Join-Path $monacoSrcDir "min"
|
||||
|
||||
if (Test-Path $targetMinDir) {
|
||||
Write-Host "Removing existing min directory..."
|
||||
Remove-Item -Recurse -Force $targetMinDir
|
||||
}
|
||||
|
||||
Write-Host "Copying new min directory..."
|
||||
Copy-Item -Recurse -Force $downloadedMinDir $targetMinDir
|
||||
|
||||
# Step 3: Generate monaco_languages.json
|
||||
Write-Host "`n--- Step 3: Generating monaco_languages.json ---"
|
||||
$generateScript = Join-Path $PSScriptRoot "generate-monaco-languages.js"
|
||||
|
||||
# Ensure playwright is available (installed in workflow, but verify here)
|
||||
$playwrightCheck = npm list playwright 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Installing playwright for headless browser..."
|
||||
npm install playwright@latest 2>&1
|
||||
npx playwright install chromium --with-deps 2>&1
|
||||
}
|
||||
|
||||
node $generateScript $monacoDir
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to generate monaco_languages.json (exit code: $LASTEXITCODE)"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $languagesJsonPath)) {
|
||||
throw "monaco_languages.json was not generated at: $languagesJsonPath"
|
||||
}
|
||||
|
||||
Write-Host "`n=== Monaco Editor update complete ==="
|
||||
Write-Host "Updated to version: $newVersion"
|
||||
Write-Host "Languages JSON: $languagesJsonPath"
|
||||
|
||||
# Output the new version for the workflow to use
|
||||
Write-Output "MONACO_VERSION=$newVersion"
|
||||
}
|
||||
finally {
|
||||
# Clean up temp directory
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
165
.github/skills/wpf-to-winui3-migration/SKILL.md
vendored
Normal file
165
.github/skills/wpf-to-winui3-migration/SKILL.md
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: wpf-to-winui3-migration
|
||||
description: Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# WPF to WinUI 3 Migration Skill
|
||||
|
||||
Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.UI.Xaml.*` / Windows App SDK). Based on patterns validated in the ImageResizer module migration.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Migrate a PowerToys module from WPF to WinUI 3
|
||||
- Convert WPF XAML files to WinUI 3 XAML
|
||||
- Replace `System.Windows` namespaces with `Microsoft.UI.Xaml`
|
||||
- Migrate `Dispatcher` usage to `DispatcherQueue`
|
||||
- Migrate custom `Observable`/`RelayCommand` to CommunityToolkit.Mvvm source generators
|
||||
- Replace WPF-UI (Lepo) controls with native WinUI 3 controls
|
||||
- Convert imaging code from `System.Windows.Media.Imaging` to `Windows.Graphics.Imaging`
|
||||
- Handle WPF `Window` vs WinUI `Window` differences (sizing, positioning, SizeToContent)
|
||||
- Migrate resource files from `.resx` to `.resw` with `ResourceLoader`
|
||||
- Fix installer/build pipeline issues after WinUI 3 migration
|
||||
- Update project files, NuGet packages, and signing config
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Visual Studio 2022 17.4+
|
||||
- Windows App SDK NuGet package (`Microsoft.WindowsAppSDK`)
|
||||
- .NET 8+ with `net8.0-windows10.0.19041.0` TFM
|
||||
- Windows 10 1803+ (April 2018 Update or newer)
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Recommended Order
|
||||
|
||||
1. **Project file** — Update TFM, NuGet packages, set `<UseWinUI>true</UseWinUI>`
|
||||
2. **Data models and business logic** — No UI dependencies, migrate first
|
||||
3. **MVVM framework** — Replace custom Observable/RelayCommand with CommunityToolkit.Mvvm
|
||||
4. **Resource strings** — Migrate `.resx` → `.resw`, introduce `ResourceLoaderInstance`
|
||||
5. **Services and utilities** — Replace `System.Windows` types, async-ify imaging code
|
||||
6. **ViewModels** — Update Dispatcher usage, binding patterns
|
||||
7. **Views/Pages** — Starting from leaf pages with fewest dependencies
|
||||
8. **Main page / shell** — Last, since it depends on everything
|
||||
9. **App.xaml / startup code** — Merge carefully (do NOT overwrite WinUI 3 boilerplate)
|
||||
10. **Installer & build pipeline** — Update WiX, signing, build events
|
||||
11. **Tests** — Adapt for WinUI 3 runtime, async patterns
|
||||
|
||||
### Key Principles
|
||||
|
||||
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different application lifecycle boilerplate. Merge your resources and initialization code into the generated WinUI 3 App class.
|
||||
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. This causes phantom build artifacts.
|
||||
- **Use `Lazy<T>` for resource-dependent statics** — `ResourceLoader` is not available at class-load time in all contexts.
|
||||
|
||||
## Quick Reference Tables
|
||||
|
||||
### Namespace Mapping
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` |
|
||||
|
||||
### Critical API Replacements
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`) |
|
||||
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` | Property vs method |
|
||||
| `Application.Current.Dispatcher` | Store `DispatcherQueue` in static field | See [Threading](./references/threading-and-windowing.md) |
|
||||
| `MessageBox.Show()` | `ContentDialog` | Must set `XamlRoot` |
|
||||
| `DynamicResource` | `ThemeResource` | Theme-reactive only |
|
||||
| `clr-namespace:` | `using:` | XAML namespace prefix |
|
||||
| `{x:Static props:Resources.Key}` | `x:Uid` or `ResourceLoader.GetString()` | .resx → .resw |
|
||||
| `DataType="{x:Type m:Foo}"` | Remove or use code-behind | `x:Type` not supported |
|
||||
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
|
||||
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
|
||||
| `SizeToContent="Height"` | Custom `SizeToContent()` via `AppWindow.Resize()` | See [Windowing](./references/threading-and-windowing.md) |
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Mouse → Pointer events |
|
||||
| `Pack URI (pack://...)` | `ms-appx:///` | Resource URI scheme |
|
||||
| `Observable` (custom base) | `ObservableObject` + `[ObservableProperty]` | CommunityToolkit.Mvvm |
|
||||
| `RelayCommand` (custom) | `[RelayCommand]` source generator | CommunityToolkit.Mvvm |
|
||||
| `JpegBitmapEncoder` | `BitmapEncoder.CreateAsync(JpegEncoderId, stream)` | Async, unified API |
|
||||
| `encoder.QualityLevel = 85` | `BitmapPropertySet { "ImageQuality", 0.85f }` | int 1-100 → float 0-1 |
|
||||
|
||||
### NuGet Package Migration
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` |
|
||||
| `WPF-UI` (Lepo) | Remove — use native WinUI 3 controls |
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` |
|
||||
| (none) | `Microsoft.WindowsAppSDK` |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` |
|
||||
| (none) | `WinUIEx` (optional, for window helpers) |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` |
|
||||
|
||||
### XAML Syntax Changes
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` |
|
||||
| `{DynamicResource Key}` | `{ThemeResource Key}` |
|
||||
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind |
|
||||
| `{x:Type local:MyType}` | Not supported |
|
||||
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` |
|
||||
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` |
|
||||
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` |
|
||||
| `<ui:FluentWindow>` / `<ui:Button>` (WPF-UI) | Native `<Window>` / `<Button>` |
|
||||
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` |
|
||||
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` |
|
||||
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / handle via KeyDown |
|
||||
| `<AccessText>` | Not available — use `AccessKey` property |
|
||||
| `<behaviors:Interaction.Triggers>` | Migrate to code-behind or WinUI behaviors |
|
||||
|
||||
## Detailed Reference Docs
|
||||
|
||||
Read only the section relevant to your current task:
|
||||
|
||||
- [Namespace and API Mapping](./references/namespace-api-mapping.md) — Full type mapping, NuGet changes, project file, CsWinRT interop
|
||||
- [XAML Migration Guide](./references/xaml-migration.md) — XAML syntax, WPF-UI removal, markup extensions, styles, resources, data binding
|
||||
- [Threading and Window Management](./references/threading-and-windowing.md) — Dispatcher, DispatcherQueue, SizeToContent, AppWindow, HWND interop, custom entry point
|
||||
- [Imaging API Migration](./references/imaging-migration.md) — BitmapEncoder/Decoder, SoftwareBitmap, CodecHelper, async patterns, int→uint
|
||||
- [PowerToys-Specific Patterns](./references/powertoys-patterns.md) — MVVM migration, ResourceLoader, Lazy init, installer, signing, test adaptation, build pipeline
|
||||
|
||||
## Common Pitfalls (from ImageResizer migration)
|
||||
|
||||
| Pitfall | Solution |
|
||||
|---------|----------|
|
||||
| `ContentDialog` throws "does not have a XamlRoot" | Set `dialog.XamlRoot = this.Content.XamlRoot` before `ShowAsync()` |
|
||||
| `FilePicker` throws error in desktop app | Call `WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)` |
|
||||
| `Window.Dispatcher` returns null | Use `Window.DispatcherQueue` instead |
|
||||
| Resources on `Window` element not found | Move resources to root layout container (`Grid.Resources`) |
|
||||
| `VisualStateManager` on `Window` fails | Use `UserControl` or `Page` inside the Window |
|
||||
| Satellite assembly installer errors (`WIX0103`) | Remove `.resources.dll` refs from `Resources.wxs`; WinUI 3 uses `.pri` |
|
||||
| Phantom `.exe`/`.deps.json` in root output dir | Avoid Exe→WinExe `ProjectReference`; use Library project |
|
||||
| `ResourceLoader` crash at static init | Wrap in `Lazy<T>` or null-coalescing property — see [Lazy Init](./references/powertoys-patterns.md#lazy-initialization-for-resource-dependent-statics) |
|
||||
| `SizeToContent` not available | Implement manual content measurement + `AppWindow.Resize()` with DPI scaling |
|
||||
| `x:Bind` default mode is `OneTime` | Explicitly set `Mode=OneWay` or `Mode=TwoWay` |
|
||||
| `DynamicResource` / `x:Static` not compiling | Replace with `ThemeResource` / `ResourceLoader` or `x:Uid` |
|
||||
| `IValueConverter.Convert` signature mismatch | Last param: `CultureInfo` → `string` (language tag) |
|
||||
| Test project can't resolve WPF types | Add `<UseWPF>true</UseWPF>` temporarily; remove after imaging migration |
|
||||
| Pixel dimension type mismatch (`int` vs `uint`) | WinRT uses `uint` for pixel sizes — add `u` suffix in test assertions |
|
||||
| `$(SolutionDir)` empty in standalone project build | Use `$(MSBuildThisFileDirectory)` with relative paths instead |
|
||||
| JPEG quality value wrong after migration | WPF: int 1-100; WinRT: float 0.0-1.0 |
|
||||
| MSIX packaging fails in PreBuildEvent | Move to PostBuildEvent; artifacts not ready at PreBuild time |
|
||||
| RC file icon path with forward slashes | Use double-backslash escaping: `..\\ui\\Assets\\icon.ico` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Build fails after namespace rename | Check for lingering `System.Windows` usings; some types have no direct equivalent |
|
||||
| Missing `PresentationCore.dll` at runtime | Ensure ALL imaging code uses `Windows.Graphics.Imaging`, not `System.Windows.Media.Imaging` |
|
||||
| `DataContext` not working on Window | WinUI 3 `Window` is not a `DependencyObject`; use a root `Page` or `UserControl` |
|
||||
| XAML designer not available | WinUI 3 does not support XAML Designer; use Hot Reload instead |
|
||||
| NuGet restore failures | Run `build-essentials.cmd` after adding `Microsoft.WindowsAppSDK` package |
|
||||
| `Parallel.ForEach` compilation error | Migrate to `Parallel.ForEachAsync` for async imaging operations |
|
||||
| Signing check fails on leaked artifacts | Run `generateAllFileComponents.ps1`; verify only `WinUI3Apps\\` paths in signing config |
|
||||
287
.github/skills/wpf-to-winui3-migration/references/imaging-migration.md
vendored
Normal file
287
.github/skills/wpf-to-winui3-migration/references/imaging-migration.md
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
# Imaging API Migration
|
||||
|
||||
Migrating from WPF (`System.Windows.Media.Imaging` / `PresentationCore.dll`) to WinRT (`Windows.Graphics.Imaging`). Based on the ImageResizer migration.
|
||||
|
||||
## Why This Migration Is Required
|
||||
|
||||
WinUI 3 apps deployed as self-contained do NOT include `PresentationCore.dll`. Any code using `System.Windows.Media.Imaging` will throw `FileNotFoundException` at runtime. ALL imaging code must use WinRT APIs.
|
||||
|
||||
| Purpose | Namespace |
|
||||
|---------|-----------|
|
||||
| UI display (`Image.Source`) | `Microsoft.UI.Xaml.Media.Imaging` |
|
||||
| Image processing (encode/decode/transform) | `Windows.Graphics.Imaging` |
|
||||
|
||||
## Architecture Change: Pipeline vs Declarative
|
||||
|
||||
The fundamental architecture differs:
|
||||
|
||||
**WPF**: In-memory pipeline of bitmap objects. Decode → transform → encode synchronously.
|
||||
```csharp
|
||||
var decoder = BitmapDecoder.Create(stream, ...);
|
||||
var transform = new TransformedBitmap(decoder.Frames[0], new ScaleTransform(...));
|
||||
var encoder = new JpegBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(transform, ...));
|
||||
encoder.Save(outputStream);
|
||||
```
|
||||
|
||||
**WinRT**: Declarative transform model. Configure transforms on the encoder, which handles pixel manipulation internally. All async.
|
||||
```csharp
|
||||
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
|
||||
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
|
||||
encoder.BitmapTransform.ScaledWidth = newWidth;
|
||||
encoder.BitmapTransform.ScaledHeight = newHeight;
|
||||
encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
|
||||
await encoder.FlushAsync();
|
||||
```
|
||||
|
||||
## Core Type Mapping
|
||||
|
||||
### Decoders
|
||||
|
||||
| WPF | WinRT | Notes |
|
||||
|-----|-------|-------|
|
||||
| `BitmapDecoder.Create(stream, options, cache)` | `BitmapDecoder.CreateAsync(stream)` | Async, auto-detects format |
|
||||
| `JpegBitmapDecoder` / `PngBitmapDecoder` / etc. | `BitmapDecoder.CreateAsync(stream)` | Single unified decoder |
|
||||
| `decoder.Frames[0]` | `await decoder.GetFrameAsync(0)` | Async frame access |
|
||||
| `decoder.Frames.Count` | `decoder.FrameCount` (uint) | `int` → `uint` |
|
||||
| `decoder.CodecInfo.ContainerFormat` | `decoder.DecoderInformation.CodecId` | Different property path |
|
||||
| `decoder.Frames[0].PixelWidth` (int) | `decoder.PixelWidth` (uint) | `int` → `uint` |
|
||||
| `WmpBitmapDecoder` | Not available | WMP/HDP not supported |
|
||||
|
||||
### Encoders
|
||||
|
||||
| WPF | WinRT | Notes |
|
||||
|-----|-------|-------|
|
||||
| `new JpegBitmapEncoder()` | `BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream)` | Async factory |
|
||||
| `new PngBitmapEncoder()` | `BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream)` | No interlace control |
|
||||
| `encoder.Frames.Add(frame)` | `encoder.SetSoftwareBitmap(bitmap)` | Different API |
|
||||
| `encoder.Save(stream)` | `await encoder.FlushAsync()` | Async |
|
||||
|
||||
### Encoder Properties (Strongly-Typed → BitmapPropertySet)
|
||||
|
||||
WPF had type-specific encoder subclasses. WinRT uses a generic property set:
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
case JpegBitmapEncoder jpeg: jpeg.QualityLevel = 85; // int 1-100
|
||||
case PngBitmapEncoder png: png.Interlace = PngInterlaceOption.On;
|
||||
case TiffBitmapEncoder tiff: tiff.Compression = TiffCompressOption.Lzw;
|
||||
|
||||
// WinRT — JPEG quality (float 0.0-1.0)
|
||||
await encoder.BitmapProperties.SetPropertiesAsync(new BitmapPropertySet
|
||||
{
|
||||
{ "ImageQuality", new BitmapTypedValue(0.85f, PropertyType.Single) }
|
||||
});
|
||||
|
||||
// WinRT — TIFF compression (via BitmapPropertySet at creation time)
|
||||
var props = new BitmapPropertySet
|
||||
{
|
||||
{ "TiffCompressionMethod", new BitmapTypedValue((byte)2, PropertyType.UInt8) }
|
||||
};
|
||||
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.TiffEncoderId, stream, props);
|
||||
```
|
||||
|
||||
**JPEG quality scale change**: WPF int `1-100` → WinRT float `0.0-1.0`. Divide by 100.
|
||||
|
||||
### Bitmap Types
|
||||
|
||||
| WPF | WinRT | Notes |
|
||||
|-----|-------|-------|
|
||||
| `BitmapSource` | `SoftwareBitmap` | Central pixel-data type |
|
||||
| `BitmapImage` | `BitmapImage` (in `Microsoft.UI.Xaml.Media.Imaging`) | UI display only |
|
||||
| `FormatConvertedBitmap` | `SoftwareBitmap.Convert()` | |
|
||||
| `TransformedBitmap` + `ScaleTransform` | `BitmapTransform` via encoder | Declarative |
|
||||
| `CroppedBitmap` | `BitmapTransform.Bounds` | |
|
||||
|
||||
### Metadata
|
||||
|
||||
| WPF | WinRT | Notes |
|
||||
|-----|-------|-------|
|
||||
| `BitmapMetadata` | `BitmapProperties` | Different API surface |
|
||||
| `BitmapMetadata.Clone()` | No equivalent | Cannot selectively clone |
|
||||
| Selective metadata removal | Not supported | All-or-nothing only |
|
||||
|
||||
**Two encoder creation strategies for metadata:**
|
||||
- `CreateForTranscodingAsync()` — preserves ALL metadata from source
|
||||
- `CreateAsync()` — creates fresh encoder with NO metadata
|
||||
|
||||
This eliminated ~258 lines of manual metadata manipulation code (`BitmapMetadataExtension.cs`) in ImageResizer.
|
||||
|
||||
### Interpolation Modes
|
||||
|
||||
| WPF `BitmapScalingMode` | WinRT `BitmapInterpolationMode` |
|
||||
|------------------------|-------------------------------|
|
||||
| `HighQuality` / `Fant` | `Fant` |
|
||||
| `Linear` | `Linear` |
|
||||
| `NearestNeighbor` | `NearestNeighbor` |
|
||||
| `Unspecified` / `LowQuality` | `Linear` |
|
||||
|
||||
## Stream Interop
|
||||
|
||||
WinRT imaging requires `IRandomAccessStream` instead of `System.IO.Stream`:
|
||||
|
||||
```csharp
|
||||
using var stream = File.OpenRead(path);
|
||||
var winrtStream = stream.AsRandomAccessStream(); // Extension method
|
||||
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
|
||||
```
|
||||
|
||||
**Critical**: For transcode, seek the input stream back to 0 before creating the encoder:
|
||||
```csharp
|
||||
winrtStream.Seek(0);
|
||||
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
|
||||
```
|
||||
|
||||
## CodecHelper Pattern (from ImageResizer)
|
||||
|
||||
WPF stored container format GUIDs in `settings.json`. WinRT uses different codec IDs. Create a `CodecHelper` to bridge them:
|
||||
|
||||
```csharp
|
||||
internal static class CodecHelper
|
||||
{
|
||||
// Maps WPF container format GUIDs (stored in settings JSON) to WinRT encoder IDs
|
||||
private static readonly Dictionary<Guid, Guid> LegacyGuidToEncoderId = new()
|
||||
{
|
||||
[new Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057")] = BitmapEncoder.JpegEncoderId,
|
||||
[new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf")] = BitmapEncoder.PngEncoderId,
|
||||
[new Guid("0af1d87e-fcfe-4188-bdeb-a7906471cbe3")] = BitmapEncoder.BmpEncoderId,
|
||||
[new Guid("163bcc30-e2e9-4f0b-961d-a3e9fdb788a3")] = BitmapEncoder.TiffEncoderId,
|
||||
[new Guid("1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5")] = BitmapEncoder.GifEncoderId,
|
||||
};
|
||||
|
||||
// Maps decoder IDs to corresponding encoder IDs
|
||||
private static readonly Dictionary<Guid, Guid> DecoderIdToEncoderId = new()
|
||||
{
|
||||
[BitmapDecoder.JpegDecoderId] = BitmapEncoder.JpegEncoderId,
|
||||
[BitmapDecoder.PngDecoderId] = BitmapEncoder.PngEncoderId,
|
||||
// ...
|
||||
};
|
||||
|
||||
public static Guid GetEncoderIdFromLegacyGuid(Guid legacyGuid)
|
||||
=> LegacyGuidToEncoderId.GetValueOrDefault(legacyGuid, Guid.Empty);
|
||||
|
||||
public static Guid GetEncoderIdForDecoder(BitmapDecoder decoder)
|
||||
=> DecoderIdToEncoderId.GetValueOrDefault(decoder.DecoderInformation.CodecId, Guid.Empty);
|
||||
}
|
||||
```
|
||||
|
||||
This preserves backward compatibility with existing `settings.json` files that contain WPF-era GUIDs.
|
||||
|
||||
## ImagingEnums Pattern (from ImageResizer)
|
||||
|
||||
WPF-specific enums (`PngInterlaceOption`, `TiffCompressOption`) from `System.Windows.Media.Imaging` are used in settings JSON. Create custom enums with identical integer values for backward-compatible deserialization:
|
||||
|
||||
```csharp
|
||||
// Replace System.Windows.Media.Imaging.PngInterlaceOption
|
||||
public enum PngInterlaceOption { Default = 0, On = 1, Off = 2 }
|
||||
|
||||
// Replace System.Windows.Media.Imaging.TiffCompressOption
|
||||
public enum TiffCompressOption { Default = 0, None = 1, Ccitt3 = 2, Ccitt4 = 3, Lzw = 4, Rle = 5, Zip = 6 }
|
||||
```
|
||||
|
||||
## Async Migration Patterns
|
||||
|
||||
### Method Signatures
|
||||
|
||||
All imaging operations become async:
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `void Execute(file, settings)` | `async Task ExecuteAsync(file, settings)` |
|
||||
| `IEnumerable<Error> Process()` | `async Task<IEnumerable<Error>> ProcessAsync()` |
|
||||
|
||||
### Parallel Processing
|
||||
|
||||
```csharp
|
||||
// WPF (synchronous)
|
||||
Parallel.ForEach(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
|
||||
(file, state, i) => { Execute(file, settings); });
|
||||
|
||||
// WinRT (async)
|
||||
await Parallel.ForEachAsync(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
|
||||
async (file, ct) => { await ExecuteAsync(file, settings); });
|
||||
```
|
||||
|
||||
### CLI Async Bridge
|
||||
|
||||
CLI entry points must bridge async to sync:
|
||||
```csharp
|
||||
return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
|
||||
```
|
||||
|
||||
### Task.Factory.StartNew → Task.Run
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
_ = Task.Factory.StartNew(StartExecutingWork, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
|
||||
// WinUI 3
|
||||
_ = Task.Run(() => StartExecutingWorkAsync());
|
||||
```
|
||||
|
||||
## SoftwareBitmap as Interface Type
|
||||
|
||||
When modules expose imaging interfaces (e.g., AI super-resolution), change parameter/return types:
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
|
||||
|
||||
// WinRT
|
||||
SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath);
|
||||
```
|
||||
|
||||
This eliminates manual `BitmapSource ↔ SoftwareBitmap` conversion code (unsafe `IMemoryBufferByteAccess` COM interop).
|
||||
|
||||
## MultiFrame Image Handling
|
||||
|
||||
```csharp
|
||||
// WinRT multi-frame encode (e.g., multi-page TIFF, animated GIF)
|
||||
for (uint i = 0; i < decoder.FrameCount; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
await encoder.GoToNextFrameAsync();
|
||||
|
||||
var frame = await decoder.GetFrameAsync(i);
|
||||
var bitmap = await frame.GetSoftwareBitmapAsync(
|
||||
frame.BitmapPixelFormat,
|
||||
BitmapAlphaMode.Premultiplied,
|
||||
transform,
|
||||
ExifOrientationMode.IgnoreExifOrientation,
|
||||
ColorManagementMode.DoNotColorManage);
|
||||
encoder.SetSoftwareBitmap(bitmap);
|
||||
}
|
||||
await encoder.FlushAsync();
|
||||
```
|
||||
|
||||
## int → uint for Pixel Dimensions
|
||||
|
||||
WinRT uses `uint` for all pixel dimensions. This affects:
|
||||
- `decoder.PixelWidth` / `decoder.PixelHeight` — `uint`
|
||||
- `BitmapTransform.ScaledWidth` / `ScaledHeight` — `uint`
|
||||
- `SoftwareBitmap` constructor — `uint` parameters
|
||||
- Test assertions: `Assert.AreEqual(96, ...)` → `Assert.AreEqual(96u, ...)`
|
||||
|
||||
## Display SoftwareBitmap in UI
|
||||
|
||||
```csharp
|
||||
var source = new SoftwareBitmapSource();
|
||||
// Must convert to Bgra8/Premultiplied for display
|
||||
if (bitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
|
||||
bitmap.BitmapAlphaMode != BitmapAlphaMode.Premultiplied)
|
||||
{
|
||||
bitmap = SoftwareBitmap.Convert(bitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
|
||||
}
|
||||
await source.SetBitmapAsync(bitmap);
|
||||
myImage.Source = source;
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
| Feature | WPF | WinRT | Impact |
|
||||
|---------|-----|-------|--------|
|
||||
| PNG interlace | `PngBitmapEncoder.Interlace` | Not available | Always non-interlaced |
|
||||
| Metadata stripping | Selective via `BitmapMetadata.Clone()` | All-or-nothing | Orientation EXIF also removed |
|
||||
| Pixel formats | Many (`Pbgra32`, `Bgr24`, `Indexed8`, ...) | Primarily `Bgra8`, `Rgba8`, `Gray8/16` | Convert to `Bgra8` |
|
||||
| WMP/HDP format | `WmpBitmapDecoder` | Not available | Not supported |
|
||||
| Pixel differences | WPF scaler | `BitmapInterpolationMode.Fant` | Not bit-identical |
|
||||
226
.github/skills/wpf-to-winui3-migration/references/namespace-api-mapping.md
vendored
Normal file
226
.github/skills/wpf-to-winui3-migration/references/namespace-api-mapping.md
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
# Namespace and API Mapping Reference
|
||||
|
||||
Complete reference for mapping WPF types to WinUI 3 equivalents, based on the ImageResizer migration.
|
||||
|
||||
## Root Namespace Mapping
|
||||
|
||||
| WPF Namespace | WinUI 3 Namespace |
|
||||
|---------------|-------------------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` |
|
||||
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` |
|
||||
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
|
||||
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
|
||||
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
|
||||
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
|
||||
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` |
|
||||
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` |
|
||||
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` |
|
||||
|
||||
## Core Type Mapping
|
||||
|
||||
| WPF Type | WinUI 3 Type |
|
||||
|----------|-------------|
|
||||
| `System.Windows.Application` | `Microsoft.UI.Xaml.Application` |
|
||||
| `System.Windows.Window` | `Microsoft.UI.Xaml.Window` (NOT a DependencyObject) |
|
||||
| `System.Windows.DependencyObject` | `Microsoft.UI.Xaml.DependencyObject` |
|
||||
| `System.Windows.DependencyProperty` | `Microsoft.UI.Xaml.DependencyProperty` |
|
||||
| `System.Windows.FrameworkElement` | `Microsoft.UI.Xaml.FrameworkElement` |
|
||||
| `System.Windows.UIElement` | `Microsoft.UI.Xaml.UIElement` |
|
||||
| `System.Windows.Visibility` | `Microsoft.UI.Xaml.Visibility` |
|
||||
| `System.Windows.Thickness` | `Microsoft.UI.Xaml.Thickness` |
|
||||
| `System.Windows.CornerRadius` | `Microsoft.UI.Xaml.CornerRadius` |
|
||||
| `System.Windows.Media.Color` | `Windows.UI.Color` (note: `Windows.UI`, not `Microsoft.UI`) |
|
||||
| `System.Windows.Media.Colors` | `Microsoft.UI.Colors` |
|
||||
|
||||
## Controls Mapping
|
||||
|
||||
### Direct Mapping (namespace-only change)
|
||||
|
||||
These controls exist in both frameworks with the same name — change `System.Windows.Controls` to `Microsoft.UI.Xaml.Controls`:
|
||||
|
||||
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
|
||||
|
||||
### Controls With Different Names or Behavior
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `MessageBox` | `ContentDialog` | Must set `XamlRoot` before `ShowAsync()` |
|
||||
| `ContextMenu` | `MenuFlyout` | Different API surface |
|
||||
| `TabControl` | `TabView` | Different API |
|
||||
| `Menu` | `MenuBar` | Different API |
|
||||
| `StatusBar` | Custom `StackPanel` layout | No built-in equivalent |
|
||||
| `AccessText` | Not available | Use `AccessKey` property on target control |
|
||||
|
||||
### WPF-UI (Lepo) to Native WinUI 3
|
||||
|
||||
ImageResizer used the `WPF-UI` library (Lepo) for Fluent styling. These must be replaced with native WinUI 3 equivalents:
|
||||
|
||||
| WPF-UI (Lepo) | WinUI 3 Native | Notes |
|
||||
|----------------|---------------|-------|
|
||||
| `<ui:FluentWindow>` | `<Window>` | Native window + `ExtendsContentIntoTitleBar` |
|
||||
| `<ui:Button>` | `<Button>` | Native button |
|
||||
| `<ui:NumberBox>` | `<NumberBox>` | Built into WinUI 3 |
|
||||
| `<ui:ProgressRing>` | `<ProgressRing>` | Built into WinUI 3 |
|
||||
| `<ui:SymbolIcon>` | `<SymbolIcon>` or `<FontIcon>` | Built into WinUI 3 |
|
||||
| `<ui:InfoBar>` | `<InfoBar>` | Built into WinUI 3 |
|
||||
| `<ui:TitleBar>` | Custom title bar via `SetTitleBar()` | Use `ExtendsContentIntoTitleBar` |
|
||||
| `<ui:ThemesDictionary>` | `<XamlControlsResources>` | In merged dictionaries |
|
||||
| `<ui:ControlsDictionary>` | Remove | Not needed — WinUI 3 has its own control styles |
|
||||
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` | Named style keys |
|
||||
|
||||
## Input Event Mapping
|
||||
|
||||
| WPF Event | WinUI 3 Event | Notes |
|
||||
|-----------|--------------|-------|
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Check `IsLeftButtonPressed` on args |
|
||||
| `MouseLeftButtonUp` | `PointerReleased` | Check pointer properties |
|
||||
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` with right button check |
|
||||
| `MouseMove` | `PointerMoved` | Uses `PointerRoutedEventArgs` |
|
||||
| `MouseWheel` | `PointerWheelChanged` | Different event args |
|
||||
| `MouseEnter` | `PointerEntered` | |
|
||||
| `MouseLeave` | `PointerExited` | |
|
||||
| `MouseDoubleClick` | `DoubleTapped` | Different event args |
|
||||
| `KeyDown` | `KeyDown` | Same name, args type: `KeyRoutedEventArgs` |
|
||||
| `PreviewKeyDown` | No direct equivalent | Use `KeyDown` with handled pattern |
|
||||
|
||||
## IValueConverter Signature Change
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `Convert(object value, Type targetType, object parameter, CultureInfo culture)` | `Convert(object value, Type targetType, object parameter, string language)` |
|
||||
| `ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)` | `ConvertBack(object value, Type targetType, object parameter, string language)` |
|
||||
|
||||
Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All converter classes must be updated.
|
||||
|
||||
## Types That Moved to Different Hierarchies
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `System.Windows.Threading.Dispatcher` | `Microsoft.UI.Dispatching.DispatcherQueue` | Completely different API |
|
||||
| `System.Windows.Threading.DispatcherPriority` | `Microsoft.UI.Dispatching.DispatcherQueuePriority` | Only 3 levels: High/Normal/Low |
|
||||
| `System.Windows.Interop.HwndSource` | `WinRT.Interop.WindowNative` | For HWND interop |
|
||||
| `System.Windows.Interop.WindowInteropHelper` | `WinRT.Interop.WindowNative.GetWindowHandle()` | |
|
||||
| `System.Windows.SystemColors` | Resource keys via `ThemeResource` | No direct static class |
|
||||
| `System.Windows.SystemParameters` | Win32 API or `DisplayInformation` | No direct equivalent |
|
||||
|
||||
## NuGet Package Migration
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| Built into .NET (no NuGet needed) | `Microsoft.WindowsAppSDK` | Required |
|
||||
| `PresentationCore` / `PresentationFramework` | `Microsoft.WinUI` (transitive) | |
|
||||
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` | |
|
||||
| `WPF-UI` (Lepo) | **Remove** — use native WinUI 3 controls | |
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) | |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` | |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` | Required |
|
||||
| (none) | `WinUIEx` | Optional, window helpers |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Extensions` | Optional |
|
||||
| (none) | `Microsoft.Web.WebView2` | If using WebView |
|
||||
|
||||
## Project File Changes
|
||||
|
||||
### WPF .csproj
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### WinUI 3 .csproj
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<SelfContained>true</SelfContained>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Assets\ImageResizer\ImageResizer.ico</ApplicationIcon>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
<ProjectPriFileName>PowerToys.ModuleName.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `UseWPF` → `UseWinUI`
|
||||
- TFM: `net8.0-windows` → `net8.0-windows10.0.19041.0`
|
||||
- Add `WindowsPackageType=None` for unpackaged desktop apps
|
||||
- Add `SelfContained=true` + `WindowsAppSDKSelfContained=true`
|
||||
- Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs` entry point
|
||||
- Set `ProjectPriFileName` to match your module's assembly name
|
||||
- Move icon from `Resources/` to `Assets/<Module>/`
|
||||
|
||||
### XAML ApplicationDefinition Setup
|
||||
|
||||
WinUI 3 requires explicit `ApplicationDefinition` declaration:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<Page Remove="ImageResizerXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="ImageResizerXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### CsWinRT Interop (for GPO and native references)
|
||||
|
||||
If the module references native C++ projects (like `GPOWrapper`):
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
Change `GPOWrapperProjection.csproj` reference to direct `GPOWrapper.vcxproj` reference.
|
||||
|
||||
### InternalsVisibleTo Migration
|
||||
|
||||
Move from code file to `.csproj`:
|
||||
|
||||
```csharp
|
||||
// DELETE: Properties/InternalsVisibleTo.cs
|
||||
// [assembly: InternalsVisibleTo("ImageResizer.Test")]
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- ADD to .csproj: -->
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ImageResizer.Test" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### Items to Remove from .csproj
|
||||
|
||||
```xml
|
||||
<!-- DELETE: WPF resource embedding -->
|
||||
<EmbeddedResource Update="Properties\Resources.resx">...</EmbeddedResource>
|
||||
<Resource Include="Resources\ImageResizer.ico" />
|
||||
<Compile Update="Properties\Resources.Designer.cs">...</Compile>
|
||||
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" /> <!-- from CLI project -->
|
||||
```
|
||||
516
.github/skills/wpf-to-winui3-migration/references/powertoys-patterns.md
vendored
Normal file
516
.github/skills/wpf-to-winui3-migration/references/powertoys-patterns.md
vendored
Normal file
@@ -0,0 +1,516 @@
|
||||
# PowerToys-Specific Migration Patterns
|
||||
|
||||
Patterns and conventions specific to the PowerToys codebase, based on the ImageResizer migration.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Before (WPF Module)
|
||||
|
||||
```
|
||||
src/modules/<module>/
|
||||
├── <Module>UI/
|
||||
│ ├── <Module>UI.csproj # OutputType=WinExe, UseWPF=true
|
||||
│ ├── App.xaml / App.xaml.cs
|
||||
│ ├── MainWindow.xaml / .cs
|
||||
│ ├── Views/
|
||||
│ ├── ViewModels/
|
||||
│ ├── Helpers/
|
||||
│ │ ├── Observable.cs # Custom INotifyPropertyChanged
|
||||
│ │ └── RelayCommand.cs # Custom ICommand
|
||||
│ ├── Properties/
|
||||
│ │ ├── Resources.resx # WPF resource strings
|
||||
│ │ ├── Resources.Designer.cs
|
||||
│ │ └── InternalsVisibleTo.cs
|
||||
│ └── Telemetry/
|
||||
├── <Module>CLI/
|
||||
│ └── <Module>CLI.csproj # OutputType=Exe
|
||||
└── tests/
|
||||
```
|
||||
|
||||
### After (WinUI 3 Module)
|
||||
|
||||
```
|
||||
src/modules/<module>/
|
||||
├── <Module>UI/
|
||||
│ ├── <Module>UI.csproj # OutputType=WinExe, UseWinUI=true
|
||||
│ ├── Program.cs # Custom entry point (DISABLE_XAML_GENERATED_MAIN)
|
||||
│ ├── app.manifest # Single manifest file
|
||||
│ ├── ImageResizerXAML/
|
||||
│ │ ├── App.xaml / App.xaml.cs # WinUI 3 App class
|
||||
│ │ ├── MainWindow.xaml / .cs
|
||||
│ │ └── Views/
|
||||
│ ├── Converters/ # WinUI 3 IValueConverter (string language)
|
||||
│ ├── ViewModels/
|
||||
│ ├── Helpers/
|
||||
│ │ └── ResourceLoaderInstance.cs # Static ResourceLoader accessor
|
||||
│ ├── Utilities/
|
||||
│ │ └── CodecHelper.cs # WPF→WinRT codec ID mapping (if imaging)
|
||||
│ ├── Models/
|
||||
│ │ └── ImagingEnums.cs # Custom enums replacing WPF imaging enums
|
||||
│ ├── Strings/
|
||||
│ │ └── en-us/
|
||||
│ │ └── Resources.resw # WinUI 3 resource strings
|
||||
│ └── Assets/
|
||||
│ └── <Module>/
|
||||
│ └── <Module>.ico # Moved from Resources/
|
||||
├── <Module>Common/ # NEW: shared library for CLI
|
||||
│ └── <Module>Common.csproj # OutputType=Library
|
||||
├── <Module>CLI/
|
||||
│ └── <Module>CLI.csproj # References Common, NOT UI
|
||||
└── tests/
|
||||
```
|
||||
|
||||
### Critical: CLI Dependency Pattern
|
||||
|
||||
**Do NOT** create `ProjectReference` from Exe to WinExe. This causes phantom build artifacts (`.exe`, `.deps.json`, `.runtimeconfig.json`) in the root output directory.
|
||||
|
||||
```
|
||||
WRONG: ImageResizerCLI (Exe) → ImageResizerUI (WinExe) ← phantom artifacts
|
||||
CORRECT: ImageResizerCLI (Exe) → ImageResizerCommon (Library)
|
||||
ImageResizerUI (WinExe) → ImageResizerCommon (Library)
|
||||
```
|
||||
|
||||
Follow the `FancyZonesCLI` → `FancyZonesEditorCommon` pattern.
|
||||
|
||||
### Files to Delete
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `Properties/Resources.resx` | Replaced by `Strings/en-us/Resources.resw` |
|
||||
| `Properties/Resources.Designer.cs` | Auto-generated; no longer needed |
|
||||
| `Properties/InternalsVisibleTo.cs` | Moved to `.csproj` `<InternalsVisibleTo>` |
|
||||
| `Helpers/Observable.cs` | Replaced by `CommunityToolkit.Mvvm.ObservableObject` |
|
||||
| `Helpers/RelayCommand.cs` | Replaced by `CommunityToolkit.Mvvm.Input` |
|
||||
| `Resources/*.ico` / `Resources/*.png` | Moved to `Assets/<Module>/` |
|
||||
| WPF `.dev.manifest` / `.prod.manifest` | Replaced by single `app.manifest` |
|
||||
| WPF-specific converters | Replaced by WinUI 3 converters with `string language` |
|
||||
|
||||
---
|
||||
|
||||
## MVVM Migration: Custom → CommunityToolkit.Mvvm Source Generators
|
||||
|
||||
### Observable Base Class → ObservableObject + [ObservableProperty]
|
||||
|
||||
**Before (custom Observable):**
|
||||
```csharp
|
||||
public class ResizeSize : Observable
|
||||
{
|
||||
private int _id;
|
||||
public int Id { get => _id; set => Set(ref _id, value); }
|
||||
|
||||
private ResizeFit _fit;
|
||||
public ResizeFit Fit
|
||||
{
|
||||
get => _fit;
|
||||
set
|
||||
{
|
||||
Set(ref _fit, value);
|
||||
UpdateShowHeight();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _showHeight = true;
|
||||
public bool ShowHeight { get => _showHeight; set => Set(ref _showHeight, value); }
|
||||
private void UpdateShowHeight() { ShowHeight = Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent; }
|
||||
}
|
||||
```
|
||||
|
||||
**After (CommunityToolkit.Mvvm source generators):**
|
||||
```csharp
|
||||
public partial class ResizeSize : ObservableObject // MUST be partial
|
||||
{
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("Id")]
|
||||
private int _id;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowHeight))] // Replaces manual UpdateShowHeight()
|
||||
private ResizeFit _fit;
|
||||
|
||||
// Computed property — no backing field, no manual update method
|
||||
public bool ShowHeight => Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- Class must be `partial` for source generators
|
||||
- `Observable` → `ObservableObject` (from CommunityToolkit.Mvvm)
|
||||
- Manual `Set(ref _field, value)` → `[ObservableProperty]` attribute
|
||||
- `PropertyChanged` dependencies → `[NotifyPropertyChangedFor(nameof(...))]`
|
||||
- Computed properties with manual `UpdateXxx()` → direct expression body
|
||||
|
||||
### Custom Name Setter with Transform
|
||||
|
||||
For properties that transform the value before storing:
|
||||
|
||||
```csharp
|
||||
// Cannot use [ObservableProperty] because of value transformation
|
||||
private string _name;
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, ReplaceTokens(value)); // SetProperty from ObservableObject
|
||||
}
|
||||
```
|
||||
|
||||
### RelayCommand → [RelayCommand] Source Generator
|
||||
|
||||
```csharp
|
||||
// DELETE: Helpers/RelayCommand.cs (custom ICommand)
|
||||
|
||||
// Before
|
||||
public ICommand ResizeCommand { get; } = new RelayCommand(Execute);
|
||||
|
||||
// After
|
||||
[RelayCommand]
|
||||
private void Resize() { /* ... */ }
|
||||
// Source generator creates ResizeCommand property automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resource String Migration (.resx → .resw)
|
||||
|
||||
### ResourceLoaderInstance Helper
|
||||
|
||||
```csharp
|
||||
internal static class ResourceLoaderInstance
|
||||
{
|
||||
internal static ResourceLoader ResourceLoader { get; private set; }
|
||||
|
||||
static ResourceLoaderInstance()
|
||||
{
|
||||
ResourceLoader = new ResourceLoader("PowerToys.ImageResizer.pri");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Use the single-argument `ResourceLoader` constructor. The two-argument version (`ResourceLoader("file.pri", "path/Resources")`) may fail if the resource map path doesn't match the actual PRI structure.
|
||||
|
||||
### Usage
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
using ImageResizer.Properties;
|
||||
string text = Resources.MyStringKey;
|
||||
|
||||
// WinUI 3
|
||||
string text = ResourceLoaderInstance.ResourceLoader.GetString("MyStringKey");
|
||||
```
|
||||
|
||||
### Lazy Initialization for Resource-Dependent Statics
|
||||
|
||||
`ResourceLoader` is not available at class-load time in all contexts (CLI mode, test harness). Use lazy initialization:
|
||||
|
||||
**Before (crashes at class load):**
|
||||
```csharp
|
||||
private static readonly CompositeFormat _format =
|
||||
CompositeFormat.Parse(Resources.Error_Format);
|
||||
|
||||
private static readonly Dictionary<string, string> _tokens = new()
|
||||
{
|
||||
["$small$"] = Resources.Small,
|
||||
["$medium$"] = Resources.Medium,
|
||||
};
|
||||
```
|
||||
|
||||
**After (lazy, safe):**
|
||||
```csharp
|
||||
private static CompositeFormat _format;
|
||||
private static CompositeFormat Format => _format ??=
|
||||
CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("Error_Format"));
|
||||
|
||||
private static readonly Lazy<Dictionary<string, string>> _tokens = new(() =>
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["$small$"] = ResourceLoaderInstance.ResourceLoader.GetString("Small"),
|
||||
["$medium$"] = ResourceLoaderInstance.ResourceLoader.GetString("Medium"),
|
||||
});
|
||||
// Usage: _tokens.Value.TryGetValue(...)
|
||||
```
|
||||
|
||||
### XAML: x:Static → x:Uid
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
<Button Content="{x:Static p:Resources.Cancel}" />
|
||||
<!-- WinUI 3 -->
|
||||
<Button x:Uid="Cancel" />
|
||||
```
|
||||
|
||||
In `.resw`, use property-suffixed keys: `Cancel.Content`, `Header.Text`, etc.
|
||||
|
||||
---
|
||||
|
||||
## CLI Options Migration
|
||||
|
||||
`System.CommandLine.Option<T>` constructor signature changed:
|
||||
|
||||
```csharp
|
||||
// WPF era — string[] aliases
|
||||
public DestinationOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Destination)
|
||||
|
||||
// WinUI 3 — single string name
|
||||
public DestinationOption()
|
||||
: base(_aliases[0], ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Destination"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installer Updates
|
||||
|
||||
### WiX Changes
|
||||
|
||||
#### 1. Remove Satellite Assembly References
|
||||
|
||||
Remove from `installer/PowerToysSetupVNext/Resources.wxs`:
|
||||
- `<Component>` entries for `<Module>.resources.dll`
|
||||
- `<RemoveFolder>` entries for locale directories
|
||||
- Module from `WinUI3AppsInstallFolder` `ParentDirectory` loop
|
||||
|
||||
#### 2. Update File Component Generation
|
||||
|
||||
Run `generateAllFileComponents.ps1` after migration. For Exe→WinExe dependency issues, add cleanup logic:
|
||||
|
||||
```powershell
|
||||
# Strip phantom ImageResizer files from BaseApplications.wxs
|
||||
$content = $content -replace 'PowerToys\.ImageResizer\.exe', ''
|
||||
$content = $content -replace 'PowerToys\.ImageResizer\.deps\.json', ''
|
||||
$content = $content -replace 'PowerToys\.ImageResizer\.runtimeconfig\.json', ''
|
||||
```
|
||||
|
||||
#### 3. Output Directory
|
||||
|
||||
WinUI 3 modules output to `WinUI3Apps/`:
|
||||
```xml
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
|
||||
```
|
||||
|
||||
### ESRP Signing
|
||||
|
||||
Update `.pipelines/ESRPSigning_core.json` — all module binaries must use `WinUI3Apps\\` paths:
|
||||
|
||||
```json
|
||||
{
|
||||
"FileList": [
|
||||
"WinUI3Apps\\PowerToys.ImageResizer.exe",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerExt.dll",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerContextMenu.dll"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Pipeline Fixes
|
||||
|
||||
### $(SolutionDir) → $(MSBuildThisFileDirectory)
|
||||
|
||||
`$(SolutionDir)` is empty when building individual projects outside the solution. Replace with relative paths from the project file:
|
||||
|
||||
```xml
|
||||
<!-- Before (breaks on standalone project build) -->
|
||||
<Exec Command="powershell $(SolutionDir)tools\build\convert-resx-to-rc.ps1" />
|
||||
|
||||
<!-- After (works always) -->
|
||||
<Exec Command="powershell $(MSBuildThisFileDirectory)..\..\..\..\tools\build\convert-resx-to-rc.ps1" />
|
||||
```
|
||||
|
||||
### MSIX Packaging: PreBuild → PostBuild
|
||||
|
||||
MSIX packaging must happen AFTER the build (artifacts not ready at PreBuild):
|
||||
|
||||
```xml
|
||||
<!-- Before -->
|
||||
<PreBuildEvent>MakeAppx.exe pack /d . /p "$(OutDir)Package.msix" /o</PreBuildEvent>
|
||||
|
||||
<!-- After -->
|
||||
<PostBuildEvent>
|
||||
if exist "$(OutDir)Package.msix" del "$(OutDir)Package.msix"
|
||||
MakeAppx.exe pack /d "$(MSBuildThisFileDirectory)." /p "$(OutDir)Package.msix" /o
|
||||
</PostBuildEvent>
|
||||
```
|
||||
|
||||
### RC File Icon Path Escaping
|
||||
|
||||
Windows Resource Compiler requires double-backslash paths:
|
||||
|
||||
```c
|
||||
// Before (breaks)
|
||||
IDI_ICON1 ICON "..\\ui\Assets\ImageResizer\ImageResizer.ico"
|
||||
// After
|
||||
IDI_ICON1 ICON "..\\ui\\Assets\\ImageResizer\\ImageResizer.ico"
|
||||
```
|
||||
|
||||
### BOM/Encoding Normalization
|
||||
|
||||
Migration may strip UTF-8 BOM from C# files (`// Copyright` → `// Copyright`). This is cosmetic and safe, but be aware it will show as changes in diff.
|
||||
|
||||
---
|
||||
|
||||
## Test Adaptation
|
||||
|
||||
### Tests Requiring WPF Runtime
|
||||
|
||||
If tests still need WPF types (e.g., comparing old vs new output), temporarily add:
|
||||
```xml
|
||||
<UseWPF>true</UseWPF>
|
||||
```
|
||||
Remove this after fully migrating all test code to WinRT APIs.
|
||||
|
||||
### Tests Using ResourceLoader
|
||||
|
||||
Unit tests cannot easily initialize WinUI 3 `ResourceLoader`. Options:
|
||||
- Hardcode expected strings in tests: `"Value must be between '{0}' and '{1}'."`
|
||||
- Delete tests that only verify resource string lookup
|
||||
- Avoid creating `App` instances in test harness (WinUI App cannot be instantiated in tests)
|
||||
|
||||
### Async Test Methods
|
||||
|
||||
All imaging tests become async:
|
||||
```csharp
|
||||
// Before
|
||||
[TestMethod]
|
||||
public void ResizesImage() { ... }
|
||||
|
||||
// After
|
||||
[TestMethod]
|
||||
public async Task ResizesImageAsync() { ... }
|
||||
```
|
||||
|
||||
### uint Assertions
|
||||
|
||||
```csharp
|
||||
// Before
|
||||
Assert.AreEqual(96, image.Frames[0].PixelWidth);
|
||||
// After
|
||||
Assert.AreEqual(96u, decoder.PixelWidth);
|
||||
```
|
||||
|
||||
### Pixel Data Access in Tests
|
||||
|
||||
```csharp
|
||||
// Before (WPF)
|
||||
public static Color GetFirstPixel(this BitmapSource source)
|
||||
{
|
||||
var pixel = new byte[4];
|
||||
new FormatConvertedBitmap(
|
||||
new CroppedBitmap(source, new Int32Rect(0, 0, 1, 1)),
|
||||
PixelFormats.Bgra32, null, 0).CopyPixels(pixel, 4, 0);
|
||||
return Color.FromArgb(pixel[3], pixel[2], pixel[1], pixel[0]);
|
||||
}
|
||||
|
||||
// After (WinRT)
|
||||
public static async Task<(byte R, byte G, byte B, byte A)> GetFirstPixelAsync(
|
||||
this BitmapDecoder decoder)
|
||||
{
|
||||
using var bitmap = await decoder.GetSoftwareBitmapAsync(
|
||||
BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
|
||||
var buffer = new Windows.Storage.Streams.Buffer(
|
||||
(uint)(bitmap.PixelWidth * bitmap.PixelHeight * 4));
|
||||
bitmap.CopyToBuffer(buffer);
|
||||
using var reader = DataReader.FromBuffer(buffer);
|
||||
byte b = reader.ReadByte(), g = reader.ReadByte(),
|
||||
r = reader.ReadByte(), a = reader.ReadByte();
|
||||
return (r, g, b, a);
|
||||
}
|
||||
```
|
||||
|
||||
### Metadata Assertions
|
||||
|
||||
```csharp
|
||||
// Before
|
||||
Assert.AreEqual("Test", ((BitmapMetadata)image.Frames[0].Metadata).Comment);
|
||||
|
||||
// After
|
||||
var props = await decoder.BitmapProperties.GetPropertiesAsync(
|
||||
new[] { "System.Photo.DateTaken" });
|
||||
Assert.IsTrue(props.ContainsKey("System.Photo.DateTaken"),
|
||||
"Metadata should be preserved during transcode");
|
||||
```
|
||||
|
||||
### AllowUnsafeBlocks for SoftwareBitmap Tests
|
||||
|
||||
If tests access pixel data via `IMemoryBufferByteAccess`, add:
|
||||
```xml
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Settings JSON Backward Compatibility
|
||||
|
||||
- Settings are stored in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
|
||||
- Schema must remain backward-compatible across upgrades
|
||||
- Add new fields with defaults; never remove or rename existing fields
|
||||
- Create custom enums matching WPF enum integer values for deserialization (e.g., `ImagingEnums.cs`)
|
||||
- See: `src/settings-ui/Settings.UI.Library/`
|
||||
|
||||
## IPC Contract
|
||||
|
||||
If the module communicates with the runner or settings UI:
|
||||
1. Update BOTH sides of the IPC contract
|
||||
2. Test settings changes are received by the module
|
||||
3. Test module state changes are reflected in settings UI
|
||||
4. Reference: `doc/devdocs/core/settings/runner-ipc.md`
|
||||
|
||||
---
|
||||
|
||||
## Checklist for PowerToys Module Migration
|
||||
|
||||
### Project & Dependencies
|
||||
- [ ] Update `.csproj`: `UseWPF` → `UseWinUI`, TFM → `net8.0-windows10.0.19041.0`
|
||||
- [ ] Add `WindowsPackageType=None`, `SelfContained=true`, `WindowsAppSDKSelfContained=true`
|
||||
- [ ] Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs`
|
||||
- [ ] Replace NuGet packages (WPF-UI → remove, add WindowsAppSDK, etc.)
|
||||
- [ ] Update project references (GPOWrapperProjection → GPOWrapper + CsWinRT)
|
||||
- [ ] Move `InternalsVisibleTo` from code to `.csproj`
|
||||
- [ ] Extract CLI shared logic to Library project (avoid Exe→WinExe dependency)
|
||||
|
||||
### MVVM & Resources
|
||||
- [ ] Replace custom `Observable`/`RelayCommand` with CommunityToolkit.Mvvm source generators
|
||||
- [ ] Migrate `.resx` → `.resw` (`Properties/Resources.resx` → `Strings/en-us/Resources.resw`)
|
||||
- [ ] Create `ResourceLoaderInstance` helper
|
||||
- [ ] Wrap resource-dependent statics in `Lazy<T>` or null-coalescing properties
|
||||
- [ ] Delete `Properties/Resources.Designer.cs`, `Observable.cs`, `RelayCommand.cs`
|
||||
|
||||
### XAML
|
||||
- [ ] Replace `clr-namespace:` → `using:` in all xmlns declarations
|
||||
- [ ] Remove WPF-UI (Lepo) xmlns and controls — use native WinUI 3
|
||||
- [ ] Replace `{x:Static p:Resources.Key}` → `x:Uid` with `.resw` keys
|
||||
- [ ] Replace `{DynamicResource}` → `{ThemeResource}`
|
||||
- [ ] Replace `DataType="{x:Type ...}"` → `x:DataType="..."`
|
||||
- [ ] Replace `<Style.Triggers>` → `VisualStateManager`
|
||||
- [ ] Add `<XamlControlsResources/>` to `App.xaml` merged dictionaries
|
||||
- [ ] Move `Window.Resources` to root container's `Resources`
|
||||
- [ ] Run XamlStyler: `.\.pipelines\applyXamlStyling.ps1 -Main`
|
||||
|
||||
### Code-Behind & APIs
|
||||
- [ ] Replace all `System.Windows.*` namespaces with `Microsoft.UI.Xaml.*`
|
||||
- [ ] Replace `Dispatcher` with `DispatcherQueue`
|
||||
- [ ] Store `DispatcherQueue` reference explicitly (no `Application.Current.Dispatcher`)
|
||||
- [ ] Implement `SizeToContent()` via AppWindow if needed
|
||||
- [ ] Update `ContentDialog` calls to set `XamlRoot`
|
||||
- [ ] Update `FilePicker` calls with HWND initialization
|
||||
- [ ] Migrate imaging code to `Windows.Graphics.Imaging` (async, `SoftwareBitmap`)
|
||||
- [ ] Create `CodecHelper` for legacy GUID → WinRT codec ID mapping (if imaging)
|
||||
- [ ] Create custom imaging enums for JSON backward compatibility (if imaging)
|
||||
- [ ] Update all `IValueConverter` signatures (`CultureInfo` → `string`)
|
||||
|
||||
### Build & Installer
|
||||
- [ ] Update WiX installer: remove satellite assembly refs from `Resources.wxs`
|
||||
- [ ] Run `generateAllFileComponents.ps1`; handle phantom artifacts
|
||||
- [ ] Update ESRP signing paths to `WinUI3Apps\\`
|
||||
- [ ] Fix `$(SolutionDir)` → `$(MSBuildThisFileDirectory)` in build events
|
||||
- [ ] Move MSIX packaging from PreBuild to PostBuild
|
||||
- [ ] Fix RC file path escaping (double-backslash)
|
||||
- [ ] Verify output dir is `WinUI3Apps/`
|
||||
|
||||
### Testing & Validation
|
||||
- [ ] Update test project: async methods, `uint` assertions
|
||||
- [ ] Handle ResourceLoader unavailability in tests (hardcode strings or skip)
|
||||
- [ ] Build clean: `cd` to project folder, `tools/build/build.cmd`, exit code 0
|
||||
- [ ] Run tests for affected module
|
||||
- [ ] Verify settings JSON backward compatibility
|
||||
- [ ] Test IPC contracts (runner ↔ settings UI)
|
||||
314
.github/skills/wpf-to-winui3-migration/references/threading-and-windowing.md
vendored
Normal file
314
.github/skills/wpf-to-winui3-migration/references/threading-and-windowing.md
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
# Threading and Window Management Migration
|
||||
|
||||
Based on patterns from the ImageResizer migration.
|
||||
|
||||
## Dispatcher → DispatcherQueue
|
||||
|
||||
### API Mapping
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `Dispatcher.Invoke(Action)` | `DispatcherQueue.TryEnqueue(Action)` |
|
||||
| `Dispatcher.BeginInvoke(Action)` | `DispatcherQueue.TryEnqueue(Action)` |
|
||||
| `Dispatcher.Invoke(DispatcherPriority, Action)` | `DispatcherQueue.TryEnqueue(DispatcherQueuePriority, Action)` |
|
||||
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` |
|
||||
| `Dispatcher.VerifyAccess()` | Check `DispatcherQueue.HasThreadAccess` (no exception-throwing method) |
|
||||
|
||||
### Priority Mapping
|
||||
|
||||
WinUI 3 has only 3 levels: `High`, `Normal`, `Low`.
|
||||
|
||||
| WPF `DispatcherPriority` | WinUI 3 `DispatcherQueuePriority` |
|
||||
|-------------------------|----------------------------------|
|
||||
| `Send` | `High` |
|
||||
| `Normal` / `Input` / `Loaded` / `Render` / `DataBind` | `Normal` |
|
||||
| `Background` / `ContextIdle` / `ApplicationIdle` / `SystemIdle` | `Low` |
|
||||
|
||||
### Pattern: Global DispatcherQueue Access (from ImageResizer)
|
||||
|
||||
WPF provided `Application.Current.Dispatcher` globally. WinUI 3 requires explicit storage:
|
||||
|
||||
```csharp
|
||||
// Store DispatcherQueue at app startup
|
||||
private static DispatcherQueue _uiDispatcherQueue;
|
||||
|
||||
public static void InitializeDispatcher()
|
||||
{
|
||||
_uiDispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
```
|
||||
|
||||
Usage with thread-check pattern (from `Settings.Reload()`):
|
||||
```csharp
|
||||
var currentDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
if (currentDispatcher != null)
|
||||
{
|
||||
// Already on UI thread
|
||||
ReloadCore(jsonSettings);
|
||||
}
|
||||
else if (_uiDispatcherQueue != null)
|
||||
{
|
||||
// Dispatch to UI thread
|
||||
_uiDispatcherQueue.TryEnqueue(() => ReloadCore(jsonSettings));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback (e.g., CLI mode, no UI)
|
||||
ReloadCore(jsonSettings);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: DispatcherQueue in ViewModels (from ProgressViewModel)
|
||||
|
||||
```csharp
|
||||
public class ProgressViewModel
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
public ProgressViewModel()
|
||||
{
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
|
||||
private void OnProgressChanged(double progress)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
Progress = progress;
|
||||
// other UI updates...
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Async Dispatch (await)
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
await this.Dispatcher.InvokeAsync(() => { /* UI work */ });
|
||||
|
||||
// WinUI 3 (using TaskCompletionSource)
|
||||
var tcs = new TaskCompletionSource();
|
||||
this.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
try { /* UI work */ tcs.SetResult(); }
|
||||
catch (Exception ex) { tcs.SetException(ex); }
|
||||
});
|
||||
await tcs.Task;
|
||||
```
|
||||
|
||||
### C++/WinRT Threading
|
||||
|
||||
| Old API | New API |
|
||||
|---------|---------|
|
||||
| `winrt::resume_foreground(CoreDispatcher)` | `wil::resume_foreground(DispatcherQueue)` |
|
||||
| `CoreDispatcher.RunAsync()` | `DispatcherQueue.TryEnqueue()` |
|
||||
|
||||
Add `Microsoft.Windows.ImplementationLibrary` NuGet for `wil::resume_foreground`.
|
||||
|
||||
---
|
||||
|
||||
## Window Management
|
||||
|
||||
### WPF Window vs WinUI 3 Window
|
||||
|
||||
| Feature | WPF `Window` | WinUI 3 `Window` |
|
||||
|---------|-------------|------------------|
|
||||
| Base class | `ContentControl` → `DependencyObject` | **NOT** a control, NOT a `DependencyObject` |
|
||||
| `Resources` property | Yes | No — use root container's `Resources` |
|
||||
| `DataContext` property | Yes | No — use root `Page`/`UserControl` |
|
||||
| `VisualStateManager` | Yes | No — use inside child controls |
|
||||
| `Load`/`Unload` events | Yes | No |
|
||||
| `SizeToContent` | Yes (`Height`/`Width`/`WidthAndHeight`) | No — must implement manually |
|
||||
| `WindowState` (min/max/normal) | Yes | No — use `AppWindow.Presenter` |
|
||||
| `WindowStyle` | Yes | No — use `AppWindow` title bar APIs |
|
||||
| `ResizeMode` | Yes | No — use `AppWindow.Presenter` |
|
||||
| `WindowStartupLocation` | Yes | No — calculate manually |
|
||||
| `Icon` | `Window.Icon` | `AppWindow.SetIcon()` |
|
||||
| `Title` | `Window.Title` | `AppWindow.Title` (or `Window.Title`) |
|
||||
| Size (Width/Height) | Yes | No — use `AppWindow.Resize()` |
|
||||
| Position (Left/Top) | Yes | No — use `AppWindow.Move()` |
|
||||
| `IsDefault`/`IsCancel` on buttons | Yes | No — handle Enter/Escape in code-behind |
|
||||
|
||||
### Getting AppWindow from Window
|
||||
|
||||
```csharp
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using WinRT.Interop;
|
||||
|
||||
IntPtr hwnd = WindowNative.GetWindowHandle(window);
|
||||
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
```
|
||||
|
||||
### Pattern: SizeToContent Replacement (from ImageResizer)
|
||||
|
||||
WinUI 3 has no `SizeToContent`. ImageResizer implemented a manual equivalent:
|
||||
|
||||
```csharp
|
||||
private void SizeToContent()
|
||||
{
|
||||
if (Content is not FrameworkElement content)
|
||||
return;
|
||||
|
||||
// Measure desired content size
|
||||
content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var desiredHeight = content.DesiredSize.Height + WindowChromeHeight + Padding;
|
||||
|
||||
// Account for DPI scaling
|
||||
var scaleFactor = Content.XamlRoot.RasterizationScale;
|
||||
var pixelHeight = (int)(desiredHeight * scaleFactor);
|
||||
var pixelWidth = (int)(WindowWidth * scaleFactor);
|
||||
|
||||
// Resize via AppWindow
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
var appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
appWindow.Resize(new Windows.Graphics.SizeInt32(pixelWidth, pixelHeight));
|
||||
}
|
||||
```
|
||||
|
||||
**Key details:**
|
||||
- `WindowChromeHeight` ≈ 32px for the title bar
|
||||
- Must multiply by `RasterizationScale` for DPI-aware sizing
|
||||
- Call `SizeToContent()` after page navigation or content changes
|
||||
- Unsubscribe previous event handlers before subscribing new ones to avoid memory leaks
|
||||
|
||||
### Window Positioning (Center Screen)
|
||||
|
||||
```csharp
|
||||
var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest);
|
||||
var centerX = (displayArea.WorkArea.Width - appWindow.Size.Width) / 2;
|
||||
var centerY = (displayArea.WorkArea.Height - appWindow.Size.Height) / 2;
|
||||
appWindow.Move(new Windows.Graphics.PointInt32(centerX, centerY));
|
||||
```
|
||||
|
||||
### Window State (Minimize/Maximize)
|
||||
|
||||
```csharp
|
||||
(appWindow.Presenter as OverlappedPresenter)?.Maximize();
|
||||
(appWindow.Presenter as OverlappedPresenter)?.Minimize();
|
||||
(appWindow.Presenter as OverlappedPresenter)?.Restore();
|
||||
```
|
||||
|
||||
### Title Bar Customization
|
||||
|
||||
```csharp
|
||||
// Extend content into title bar
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.SetTitleBar(AppTitleBar); // AppTitleBar is a XAML element
|
||||
|
||||
// Or via AppWindow API
|
||||
if (AppWindowTitleBar.IsCustomizationSupported())
|
||||
{
|
||||
var titleBar = appWindow.TitleBar;
|
||||
titleBar.ExtendsContentIntoTitleBar = true;
|
||||
titleBar.ButtonBackgroundColor = Colors.Transparent;
|
||||
}
|
||||
```
|
||||
|
||||
### Tracking the Main Window
|
||||
|
||||
```csharp
|
||||
public partial class App : Application
|
||||
{
|
||||
public static Window MainWindow { get; private set; }
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
MainWindow = new MainWindow();
|
||||
MainWindow.Activate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ContentDialog Requires XamlRoot
|
||||
|
||||
```csharp
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = "Confirm",
|
||||
Content = "Are you sure?",
|
||||
PrimaryButtonText = "Yes",
|
||||
CloseButtonText = "No",
|
||||
XamlRoot = this.Content.XamlRoot // REQUIRED
|
||||
};
|
||||
var result = await dialog.ShowAsync();
|
||||
```
|
||||
|
||||
### File Pickers Require HWND
|
||||
|
||||
```csharp
|
||||
var picker = new FileOpenPicker();
|
||||
picker.FileTypeFilter.Add(".jpg");
|
||||
|
||||
// REQUIRED for desktop apps
|
||||
var hwnd = WindowNative.GetWindowHandle(App.MainWindow);
|
||||
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
|
||||
|
||||
var file = await picker.PickSingleFileAsync();
|
||||
```
|
||||
|
||||
### Window Close Handling
|
||||
|
||||
```csharp
|
||||
// WPF
|
||||
protected override void OnClosing(CancelEventArgs e) { e.Cancel = true; this.Hide(); }
|
||||
|
||||
// WinUI 3
|
||||
this.AppWindow.Closing += (s, e) => { e.Cancel = true; this.AppWindow.Hide(); };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Entry Point (DISABLE_XAML_GENERATED_MAIN)
|
||||
|
||||
ImageResizer uses a custom `Program.cs` entry point instead of the WinUI 3 auto-generated `Main`. This is needed for:
|
||||
- CLI mode (process files without showing UI)
|
||||
- Custom initialization before the WinUI 3 App starts
|
||||
- Single-instance enforcement
|
||||
|
||||
### Setup
|
||||
|
||||
In `.csproj`:
|
||||
```xml
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
```
|
||||
|
||||
Create `Program.cs`:
|
||||
```csharp
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0)
|
||||
{
|
||||
// CLI mode — no UI
|
||||
return RunCli(args);
|
||||
}
|
||||
|
||||
// GUI mode
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
Application.Start((p) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(
|
||||
DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WPF App Constructor Removal
|
||||
|
||||
WPF modules often created `new App()` to initialize the WPF `Application` and get `Application.Current.Dispatcher`. This is no longer needed — the WinUI 3 `Application.Start()` handles this.
|
||||
|
||||
```csharp
|
||||
// DELETE (WPF pattern):
|
||||
_imageResizerApp = new App();
|
||||
// REPLACE with: Store DispatcherQueue explicitly (see Global DispatcherQueue Access above)
|
||||
```
|
||||
365
.github/skills/wpf-to-winui3-migration/references/xaml-migration.md
vendored
Normal file
365
.github/skills/wpf-to-winui3-migration/references/xaml-migration.md
vendored
Normal file
@@ -0,0 +1,365 @@
|
||||
# XAML Migration Guide
|
||||
|
||||
Detailed reference for migrating XAML from WPF to WinUI 3, based on the ImageResizer migration.
|
||||
|
||||
## XML Namespace Declaration Changes
|
||||
|
||||
### Before (WPF)
|
||||
|
||||
```xml
|
||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:MyApp"
|
||||
xmlns:m="clr-namespace:ImageResizer.Models"
|
||||
xmlns:p="clr-namespace:ImageResizer.Properties"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
x:Class="MyApp.MainWindow">
|
||||
```
|
||||
|
||||
### After (WinUI 3)
|
||||
|
||||
```xml
|
||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:MyApp"
|
||||
xmlns:m="using:ImageResizer.Models"
|
||||
xmlns:converters="using:ImageResizer.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
x:Class="MyApp.MainWindow">
|
||||
```
|
||||
|
||||
### Key Changes
|
||||
|
||||
| WPF Syntax | WinUI 3 Syntax | Notes |
|
||||
|------------|---------------|-------|
|
||||
| `clr-namespace:Foo` | `using:Foo` | CLR namespace mapping |
|
||||
| `clr-namespace:Foo;assembly=Bar` | `using:Foo` | Assembly qualification not needed |
|
||||
| `xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"` | **Remove entirely** | WPF-UI namespace no longer needed |
|
||||
| `xmlns:p="clr-namespace:...Properties"` | **Remove** | No more `.resx` string bindings |
|
||||
| `sys:String` (from mscorlib) | `x:String` | XAML intrinsic types |
|
||||
| `sys:Int32` | `x:Int32` | XAML intrinsic types |
|
||||
| `sys:Boolean` | `x:Boolean` | XAML intrinsic types |
|
||||
| `sys:Double` | `x:Double` | XAML intrinsic types |
|
||||
|
||||
## Unsupported Markup Extensions
|
||||
|
||||
| WPF Markup Extension | WinUI 3 Alternative |
|
||||
|----------------------|---------------------|
|
||||
| `{DynamicResource Key}` | `{ThemeResource Key}` (theme-reactive) or `{StaticResource Key}` |
|
||||
| `{x:Static Type.Member}` | `{x:Bind}` to a static property, or code-behind |
|
||||
| `{x:Type local:MyType}` | Not supported; use code-behind |
|
||||
| `{x:Array}` | Not supported; create collections in code-behind |
|
||||
| `{x:Code}` | Not supported |
|
||||
|
||||
### DynamicResource → ThemeResource
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
<TextBlock Foreground="{DynamicResource MyBrush}" />
|
||||
|
||||
<!-- WinUI 3 -->
|
||||
<TextBlock Foreground="{ThemeResource MyBrush}" />
|
||||
```
|
||||
|
||||
`ThemeResource` automatically updates when the app theme changes (Light/Dark/HighContrast). For truly dynamic non-theme resources, set values in code-behind or use data binding.
|
||||
|
||||
### x:Static Resource Strings → x:Uid
|
||||
|
||||
This is the most pervasive XAML change. WPF used `{x:Static}` to bind to strongly-typed `.resx` resource strings. WinUI 3 uses `x:Uid` with `.resw` files.
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<Button Content="{x:Static p:Resources.Cancel}" />
|
||||
<TextBlock Text="{x:Static p:Resources.Input_Header}" />
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<Button x:Uid="Cancel" />
|
||||
<TextBlock x:Uid="Input_Header" />
|
||||
```
|
||||
|
||||
In `Strings/en-us/Resources.resw`:
|
||||
```xml
|
||||
<data name="Cancel.Content" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="Input_Header.Text" xml:space="preserve">
|
||||
<value>Select a size</value>
|
||||
</data>
|
||||
```
|
||||
|
||||
The `x:Uid` suffix (`.Content`, `.Text`, `.Header`, `.PlaceholderText`, etc.) matches the target property name.
|
||||
|
||||
### DataType with x:Type → Remove
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<DataTemplate DataType="{x:Type m:ResizeSize}">
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<DataTemplate x:DataType="m:ResizeSize">
|
||||
```
|
||||
|
||||
## WPF-UI (Lepo) Controls Removal
|
||||
|
||||
If the module uses the `WPF-UI` library, replace all Lepo controls with native WinUI 3 equivalents.
|
||||
|
||||
### Window
|
||||
|
||||
```xml
|
||||
<!-- WPF (WPF-UI) -->
|
||||
<ui:FluentWindow
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<ui:TitleBar Title="Image Resizer" />
|
||||
...
|
||||
</ui:FluentWindow>
|
||||
|
||||
<!-- WinUI 3 (native) -->
|
||||
<Window>
|
||||
<!-- Title bar managed via code-behind: this.ExtendsContentIntoTitleBar = true; -->
|
||||
...
|
||||
</Window>
|
||||
```
|
||||
|
||||
### App.xaml Resources
|
||||
|
||||
```xml
|
||||
<!-- WPF (WPF-UI) -->
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemesDictionary Theme="Dark" />
|
||||
<ui:ControlsDictionary />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<!-- WinUI 3 (native) -->
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
### Common Control Replacements
|
||||
|
||||
```xml
|
||||
<!-- WPF-UI NumberBox -->
|
||||
<ui:NumberBox Value="{Binding Width}" />
|
||||
<!-- WinUI 3 -->
|
||||
<NumberBox Value="{x:Bind ViewModel.Width, Mode=TwoWay}" />
|
||||
|
||||
<!-- WPF-UI InfoBar -->
|
||||
<ui:InfoBar Title="Warning" Message="..." IsOpen="True" Severity="Warning" />
|
||||
<!-- WinUI 3 -->
|
||||
<InfoBar Title="Warning" Message="..." IsOpen="True" Severity="Warning" />
|
||||
|
||||
<!-- WPF-UI ProgressRing -->
|
||||
<ui:ProgressRing IsIndeterminate="True" />
|
||||
<!-- WinUI 3 -->
|
||||
<ProgressRing IsActive="True" />
|
||||
|
||||
<!-- WPF-UI SymbolIcon -->
|
||||
<ui:SymbolIcon Symbol="Add" />
|
||||
<!-- WinUI 3 -->
|
||||
<SymbolIcon Symbol="Add" />
|
||||
```
|
||||
|
||||
### Button Patterns
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
<Button IsDefault="True" Content="OK" />
|
||||
<Button IsCancel="True" Content="Cancel" />
|
||||
|
||||
<!-- WinUI 3 (no IsDefault/IsCancel) -->
|
||||
<Button Style="{StaticResource AccentButtonStyle}" Content="OK" />
|
||||
<Button Content="Cancel" />
|
||||
<!-- Handle Enter/Escape keys in code-behind if needed -->
|
||||
```
|
||||
|
||||
## Style and Template Changes
|
||||
|
||||
### Triggers → VisualStateManager
|
||||
|
||||
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<Style TargetType="Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="LightBlue"/>
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="PointerOver">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.Background" Value="LightBlue"/>
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<ContentPresenter />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
```
|
||||
|
||||
### No Binding in Setter.Value
|
||||
|
||||
```xml
|
||||
<!-- WPF (works) -->
|
||||
<Setter Property="Foreground" Value="{Binding TextColor}"/>
|
||||
|
||||
<!-- WinUI 3 (does NOT work — use StaticResource) -->
|
||||
<Setter Property="Foreground" Value="{StaticResource TextColorBrush}"/>
|
||||
```
|
||||
|
||||
### Visual State Name Changes
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `MouseOver` | `PointerOver` |
|
||||
| `Disabled` | `Disabled` |
|
||||
| `Pressed` | `Pressed` |
|
||||
|
||||
## Resource Dictionary Changes
|
||||
|
||||
### Window.Resources → Grid.Resources
|
||||
|
||||
WinUI 3 `Window` is NOT a `DependencyObject` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
<Window>
|
||||
<Window.Resources>
|
||||
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
|
||||
</Window.Resources>
|
||||
<Grid>...</Grid>
|
||||
</Window>
|
||||
|
||||
<!-- WinUI 3 -->
|
||||
<Window>
|
||||
<Grid>
|
||||
<Grid.Resources>
|
||||
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
|
||||
</Grid.Resources>
|
||||
...
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### Theme Dictionaries
|
||||
|
||||
```xml
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="MyBrush" Color="#FF000000"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="MyBrush" Color="#FFFFFFFF"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="MyBrush" Color="{ThemeResource SystemColorWindowTextColor}"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
## URI Scheme Changes
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `pack://application:,,,/MyAssembly;component/image.png` | `ms-appx:///Assets/image.png` |
|
||||
| `pack://application:,,,/image.png` | `ms-appx:///image.png` |
|
||||
| Relative path `../image.png` | `ms-appx:///image.png` |
|
||||
|
||||
Assets directory convention: `Resources/` → `Assets/<Module>/`
|
||||
|
||||
## Data Binding Changes
|
||||
|
||||
### {Binding} vs {x:Bind}
|
||||
|
||||
Both are available. Prefer `{x:Bind}` for compile-time safety and performance.
|
||||
|
||||
| Feature | `{Binding}` | `{x:Bind}` |
|
||||
|---------|------------|------------|
|
||||
| Default mode | `OneWay` | **`OneTime`** (explicit `Mode=OneWay` required!) |
|
||||
| Context | `DataContext` | Code-behind class |
|
||||
| Resolution | Runtime | Compile-time |
|
||||
| Performance | Reflection-based | Compiled |
|
||||
| Function binding | No | Yes |
|
||||
|
||||
### WPF-Specific Binding Features to Remove
|
||||
|
||||
```xml
|
||||
<!-- These WPF-only features must be removed or rewritten -->
|
||||
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<!-- WinUI 3: UpdateSourceTrigger not needed; TextBox uses PropertyChanged by default -->
|
||||
<TextBox Text="{x:Bind ViewModel.Value, Mode=TwoWay}" />
|
||||
|
||||
{Binding RelativeSource={RelativeSource Self}, ...}
|
||||
<!-- WinUI 3: Use x:Bind which binds to the page itself, or use ElementName -->
|
||||
|
||||
<ItemsControl ItemsSource="{Binding}" />
|
||||
<!-- WinUI 3: Must specify explicit path -->
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.Items}" />
|
||||
```
|
||||
|
||||
## WPF-Only Window Properties to Remove
|
||||
|
||||
These properties exist on WPF `Window` but not WinUI 3:
|
||||
|
||||
```xml
|
||||
<!-- Remove from XAML — handle in code-behind via AppWindow API -->
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
ExtendsContentIntoTitleBar="True" <!-- Set in code-behind -->
|
||||
```
|
||||
|
||||
## XAML Control Property Changes
|
||||
|
||||
| WPF Property | WinUI 3 Property | Notes |
|
||||
|-------------|-----------------|-------|
|
||||
| `Focusable` | `IsTabStop` | Different name |
|
||||
| `SnapsToDevicePixels` | Not available | WinUI handles pixel snapping internally |
|
||||
| `UseLayoutRounding` | `UseLayoutRounding` | Same |
|
||||
| `IsHitTestVisible` | `IsHitTestVisible` | Same |
|
||||
| `TextBox.VerticalScrollBarVisibility` | `ScrollViewer.VerticalScrollBarVisibility` (attached) | Attached property |
|
||||
|
||||
## XAML Formatting (XamlStyler)
|
||||
|
||||
After migration, run XamlStyler to normalize formatting:
|
||||
- Alphabetize xmlns declarations and element attributes
|
||||
- Add UTF-8 BOM to all XAML files
|
||||
- Normalize comment spacing: `<!-- text -->` → `<!-- text -->`
|
||||
|
||||
PowerToys command: `.\.pipelines\applyXamlStyling.ps1 -Main`
|
||||
134
.github/workflows/update-monaco-editor.yml
vendored
Normal file
134
.github/workflows/update-monaco-editor.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
# Update Monaco Editor
|
||||
#
|
||||
# Automates the Monaco Editor update process described in
|
||||
# doc/devdocs/common/FilePreviewCommon.md:
|
||||
# 1. Downloads the latest (or specified) Monaco Editor from npm
|
||||
# 2. Replaces src/Monaco/monacoSRC/min with the new version
|
||||
# 3. Regenerates monaco_languages.json
|
||||
# 4. Runs validation tests
|
||||
# 5. Creates a pull request with the changes
|
||||
#
|
||||
# Trigger manually via workflow_dispatch.
|
||||
|
||||
name: Update Monaco Editor
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Monaco Editor version (e.g. "0.50.0"). Leave empty for latest.'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-monaco:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
npm install playwright@latest
|
||||
npx playwright install chromium --with-deps
|
||||
|
||||
- name: Get current Monaco version
|
||||
id: current_version
|
||||
shell: bash
|
||||
run: |
|
||||
CURRENT=$(grep -oP 'Version:\s*\K[\d.]+' src/Monaco/monacoSRC/min/vs/loader.js || echo "unknown")
|
||||
echo "version=$CURRENT" >> "$GITHUB_OUTPUT"
|
||||
echo "Current Monaco version: $CURRENT"
|
||||
|
||||
- name: Run Monaco update script
|
||||
id: update
|
||||
shell: pwsh
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
$version = $env:INPUT_VERSION
|
||||
if ([string]::IsNullOrWhiteSpace($version)) {
|
||||
$version = 'latest'
|
||||
}
|
||||
$output = & ./.github/scripts/update-monaco-editor.ps1 -Version $version -RepoRoot $env:GITHUB_WORKSPACE
|
||||
# Extract version from output
|
||||
$versionLine = $output | Select-String -Pattern '^MONACO_VERSION=' | Select-Object -First 1
|
||||
if ($versionLine) {
|
||||
$newVersion = $versionLine.ToString().Split('=')[1]
|
||||
echo "new_version=$newVersion" >> $env:GITHUB_OUTPUT
|
||||
}
|
||||
|
||||
- name: Run validation tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
./.github/scripts/tests/validate-monaco-update.tests.ps1 -RepoRoot $env:GITHUB_WORKSPACE
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
shell: bash
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No changes detected - Monaco may already be up to date."
|
||||
else
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
CHANGED_FILES=$(git diff --stat | tail -1)
|
||||
echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
# 3rd-party action pinned to commit hash per Microsoft security guidelines
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update Monaco Editor to ${{ steps.update.outputs.new_version }}"
|
||||
title: "Update Monaco Editor from ${{ steps.current_version.outputs.version }} to ${{ steps.update.outputs.new_version }}"
|
||||
body: |
|
||||
## Summary
|
||||
|
||||
Automated update of the Monaco Editor dependency.
|
||||
|
||||
**Previous version:** ${{ steps.current_version.outputs.version }}
|
||||
**New version:** ${{ steps.update.outputs.new_version }}
|
||||
|
||||
## Changes
|
||||
|
||||
- Updated `src/Monaco/monacoSRC/min/` with the new Monaco Editor release
|
||||
- Regenerated `src/Monaco/monaco_languages.json`
|
||||
|
||||
## Validation
|
||||
|
||||
The following automated checks passed:
|
||||
- ✅ `loader.js` contains valid version header
|
||||
- ✅ `monaco_languages.json` is valid JSON with expected structure
|
||||
- ✅ All expected built-in languages are present
|
||||
- ✅ All PowerToys custom languages (reg, gitignore, srt) are present
|
||||
- ✅ All custom language extensions are registered
|
||||
- ✅ Monaco directory structure is intact
|
||||
|
||||
## Manual Verification
|
||||
|
||||
Before merging, please verify:
|
||||
- [ ] File Explorer Dev File Preview works correctly
|
||||
- [ ] Peek module previews code files properly
|
||||
- [ ] Registry Preview module functions normally
|
||||
|
||||
## Reference
|
||||
|
||||
- [Monaco Editor update docs](doc/devdocs/common/FilePreviewCommon.md#update-monaco-editor)
|
||||
- [Monaco Editor releases](https://github.com/microsoft/monaco-editor/releases)
|
||||
branch: automated/update-monaco-editor
|
||||
delete-branch: true
|
||||
labels: |
|
||||
dependencies
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -360,3 +360,11 @@ src/common/Telemetry/*.etl
|
||||
# PowerToysInstaller Build Temp Files
|
||||
installer/*/*.wxs.bk
|
||||
/src/modules/awake/.claude
|
||||
|
||||
# Claude AI local settings - local-only, not committed
|
||||
**/.claude/settings.local.json
|
||||
|
||||
# Squad / Copilot agents — local-only, not committed
|
||||
.squad/
|
||||
.squad-workstream
|
||||
.github/agents/
|
||||
|
||||
@@ -427,7 +427,7 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/FileLocksmith/">
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj" Id="49D456D3-F485-45AF-8875-45B44F193DDC" />
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj" Id="49d456d3-f485-45af-8875-45b44f193ddc" />
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithContextMenu/FileLocksmithContextMenu.vcxproj" Id="799a50d8-de89-4ed1-8ff8-ad5a9ed8c0ca" />
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj" Id="57175ec7-92a5-4c1e-8244-e3fbca2a81de" />
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj" Id="9d52fd25-ef90-4f9a-a015-91efc5daf54f" />
|
||||
@@ -438,7 +438,7 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/FileLocksmith/Tests/">
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj" Id="A1B2C3D4-E5F6-7890-1234-567890ABCDEF" />
|
||||
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-1234-567890abcdef" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/Hosts/">
|
||||
<Project Path="src/modules/Hosts/Hosts/Hosts.csproj">
|
||||
@@ -464,13 +464,13 @@
|
||||
</Folder>
|
||||
<Folder Name="/modules/imageresizer/">
|
||||
<Project Path="src/modules/imageresizer/dll/ImageResizerExt.vcxproj" Id="0b43679e-edfa-4da0-ad30-f4628b308b1b" />
|
||||
<Project Path="src/modules/imageresizer/ImageResizerContextMenu/ImageResizerContextMenu.vcxproj" Id="93b72a06-c8bd-484f-a6f7-c9f280b150bf" />
|
||||
<Project Path="src/modules/imageresizer/ImageResizerLib/ImageResizerLib.vcxproj" Id="18b3db45-4ffe-4d01-97d6-5223feee1853" />
|
||||
<Project Path="src/modules/imageresizer/ui/ImageResizerUI.csproj">
|
||||
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/imageresizer/ImageResizerCLI/ImageResizerCLI.csproj">
|
||||
<Project Path="src/modules/imageresizer/ImageResizerContextMenu/ImageResizerContextMenu.vcxproj" Id="93b72a06-c8bd-484f-a6f7-c9f280b150bf" />
|
||||
<Project Path="src/modules/imageresizer/ImageResizerLib/ImageResizerLib.vcxproj" Id="18b3db45-4ffe-4d01-97d6-5223feee1853" />
|
||||
<Project Path="src/modules/imageresizer/ui/ImageResizerUI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
@@ -1027,7 +1027,10 @@
|
||||
<File Path="src/modules/Workspaces/workspaces-common/WindowUtils.h" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/ZoomIt/">
|
||||
<Project Path="src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj" Id="0a84f764-3a88-44cd-aa96-41bdbd48627b" />
|
||||
<Project Path="src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj" Id="0a84f764-3a88-44cd-aa96-41bdbd48627b">
|
||||
<BuildDependency Project="src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.vcxproj" />
|
||||
</Project>
|
||||
<Project Path="src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.vcxproj" Id="94ba3051-c8d7-454a-9d46-1a7c78e228a3" />
|
||||
<Project Path="src/modules/ZoomIt/ZoomItModuleInterface/ZoomItModuleInterface.vcxproj" Id="e4585179-2ac1-4d5f-a3ff-cfc5392f694c" />
|
||||
<Project Path="src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettingsInterop.vcxproj" Id="ca7d8106-30b9-4aec-9d05-b69b31b8c461" />
|
||||
</Folder>
|
||||
|
||||
256
README.md
256
README.md
@@ -51,19 +51,19 @@ But to get started quickly, choose one of the installation methods below:
|
||||
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.2/PowerToysUserSetup-0.97.2-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.2/PowerToysUserSetup-0.97.2-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.2/PowerToysSetup-0.97.2-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.2/PowerToysSetup-0.97.2-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.99%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.97.2-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.97.2-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.97.2-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.97.2-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.98.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.98.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.98.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.98.1-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -102,242 +102,14 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
## ✨ What's new?
|
||||
|
||||
**Version 0.97.2 (Feb 2026)**
|
||||
[](https://github.com/microsoft/PowerToys/releases)
|
||||
|
||||
This patch release fixes several important stability issues identified in v0.97.0 based on incoming reports. Check out the [v0.97.0](https://github.com/microsoft/PowerToys/releases/tag/v0.97.0) notes for the full list of changes.
|
||||
|
||||
## Advanced Paste
|
||||
- #45207 Fixed a crash in the Advanced Paste settings page caused by null values during JSON deserialization.
|
||||
|
||||
## Color Picker
|
||||
- #45367 Fixed contrast issue in Color picker UI.
|
||||
|
||||
## Command Palette
|
||||
- #45194 Fixed an issue where some Command Palette PowerToys Extension strings were not localised.
|
||||
|
||||
## Cursor Wrap
|
||||
- #45210 Fixed "Automatically activate on utility startup" setting not persisting when disabled. Thanks [@ThanhNguyxn](https://github.com/ThanhNguyxn)!
|
||||
- #45303 Added option to disable Cursor Wrapping when only a single monitor is connected. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
## Image Resizer
|
||||
- #45184 Fixed Image Resizer not working after upgrading PowerToys on Windows 10 by properly cleaning up legacy sparse app packages.
|
||||
|
||||
## LightSwitch
|
||||
- #45304 Fixed Light Switch startup logic to correctly apply the appropriate theme on launch.
|
||||
|
||||
## Workspaces
|
||||
- #45183 Fixed overlay positioning issue in workspace snapshot draw caused by DPI-aware coordinate mismatch.
|
||||
|
||||
## Quick Access and Measure Tool
|
||||
- #45443 Fixed crash related to `IsShownInSwitchers` property when Explorer is not running.
|
||||
|
||||
**Version 0.97.1 (January 2026)**
|
||||
|
||||
**Highlights**
|
||||
|
||||
### Advanced Paste
|
||||
- #44862: Fixed Settings UI advanced paste page crash by using correct settings repository for null checking.
|
||||
|
||||
### Command Palette
|
||||
- #44886: Fixed personalization section not appearing by using latest MSIX for installation.
|
||||
- #44938: Fixed loading of icons from internet shortcuts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- #45076: Fixed potential deadlock from lazy-loading AppListItem details. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Cursor Wrap
|
||||
- #44936: Added improved multi-monitor support; Added laptop lid close detection for dynamic monitor topology updates. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- #44936: Added new settings dropdown to constrain wrapping to horizontal-only, vertical-only, or both directions. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Peek
|
||||
- #44995: Fixed Space key triggering Peek during file rename, search, or address bar typing.
|
||||
|
||||
### PowerRename
|
||||
- #44944: Fixed regex `$` not working, preventing users from adding text at the end of filenames.
|
||||
|
||||
### Runner
|
||||
- #44931: Monochrome tray icon now adapts to Windows system theme instead of app theme.
|
||||
- #44982: Fixed right-click menu to dynamically update based on Quick Access enabled/disabled state.
|
||||
|
||||
### GPO / Enterprise
|
||||
- #45028: Added CursorWrap policy definition to ADMX templates. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
|
||||
For the full list of v0.97 changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
## Advanced Paste
|
||||
|
||||
- Added hex color previews in clipboard history. Thanks [@crramirez](https://github.com/crramirez)!
|
||||
- Added automatic placeholder endpoints when required fields are left empty.
|
||||
- Fixed a grammar issue in the AI settings description. Thanks [@erik-anderson](https://github.com/erik-anderson)!
|
||||
- Fixed loading order so custom action hotkeys are read correctly.
|
||||
- Updated Advanced Paste descriptions to reflect support for online and local models.
|
||||
- Fixed clipboard history item selection so it doesn’t duplicate entries.
|
||||
- Prevented placeholder endpoints from being saved for providers that don’t need them.
|
||||
- Added image input support for AI transforms and improved clipboard change tracking.
|
||||
|
||||
## Awake
|
||||
|
||||
- Fixed Awake CLI so help, errors, and logs appear correctly in the console. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
## Command Palette
|
||||
|
||||
- Fixed background image loading in BlurImageControl. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed SDK packaging paths and added a CI SDK build stage.
|
||||
- Aligned naming and spell-checking with .NET conventions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added drag-and-drop support for Command Palette items. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a PowerToys Command Palette extension to discover and launch PowerToys utilities.
|
||||
- Fixed grid view bindings and layout issues. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed a line-break issue in RDC extension toast messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made the Settings button text localizable. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Hid the RDC fallback on the home page and fixed MSTSC working directory handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Optimized result list merging for better performance. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added Small/Medium/Large detail sizes in the extensions API. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
- Hid fallback commands on the home page when no query is entered. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added back navigation support in the Settings window. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a Command Palette solution filter. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated Extension SDK documentation links to Microsoft Learn. Thanks [@RubenFricke](https://github.com/RubenFricke)!
|
||||
- Added a custom search engine URL setting for Web Search. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added pinyin matching for Chinese input. Thanks [@frg2089](https://github.com/frg2089)!
|
||||
- Bumped Command Palette version to 0.8.
|
||||
- Removed subtitles from built-in top-level commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Refined separator styling in the details pane. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a built-in Remote Desktop extension.
|
||||
- Added a Peek command to the Indexer extension.
|
||||
- Improved default browser detection using the Windows Shell API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Escape key behavior options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added theme and background customization options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved WinGet package app matching. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added an auto-return-home delay setting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added fallback ranking and global results settings.
|
||||
- Removed the selection indicator in the context menu list. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a developer ribbon with build and log info. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated the “Learn more” string for Command Palette. Thanks [@pratnala](https://github.com/pratnala)!
|
||||
- Added arrow-key navigation for grid views. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- Fixed version display when running unpackaged. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added a native debugging launch profile. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Reduced redundant property change notifications in the SDK. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved section readability and accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made gallery spacing uniform. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added sections and separators for list and grid pages. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
|
||||
## Crop & Lock
|
||||
|
||||
- Added a screenshot mode that freezes a cropped region into its own window. Thanks [@fm-sys](https://github.com/fm-sys)!
|
||||
|
||||
## Cursor Wrap
|
||||
|
||||
- Improved Cursor Wrap behavior on multi-monitor setups by wrapping only at outer edges. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
## FancyZones
|
||||
|
||||
- Fixed editor overlay positioning on mixed-DPI multi-monitor setups. Thanks [@Memphizzz](https://github.com/Memphizzz)!
|
||||
- Added a FancyZones CLI for command-line layout management.
|
||||
|
||||
## File Locksmith
|
||||
|
||||
- Added a File Locksmith CLI for querying, waiting on, or killing file locks.
|
||||
|
||||
## Find My Mouse
|
||||
|
||||
- Improved spotlight edge rendering for clearer Find My Mouse visuals.
|
||||
- Added telemetry to track how Find My Mouse is triggered.
|
||||
|
||||
## Image Resizer
|
||||
|
||||
- Fixed Fill mode cropping when Shrink Only is enabled. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a dedicated Image Resizer CLI for scripted resizing.
|
||||
|
||||
## Light Switch
|
||||
|
||||
- Added telemetry events for Light Switch usage and settings changes.
|
||||
- Added a Follow Night Light mode to sync theme changes with Night Light.
|
||||
- Clarified LightSwitchService and LightSwitchStateManager roles in docs.
|
||||
- Added a Quick Access dashboard button to toggle Light Switch quickly.
|
||||
- Ensured Light Switch honors GPO policy states with clear status messaging.
|
||||
|
||||
## Mouse Without Borders
|
||||
|
||||
- Continued refactoring Mouse Without Borders by splitting the large Common class into focused components. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
- Completed the Common class refactor with Core and IPC helper extraction. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
## Peek
|
||||
|
||||
- Hardened Peek previews with strict resource filtering and safer external link warnings.
|
||||
- Improved SVG preview compatibility by rendering via WebView2.
|
||||
|
||||
## PowerRename
|
||||
|
||||
- Added HEIF/AVIF EXIF metadata extraction and extension status guidance for related previews.
|
||||
- Fixed undefined behavior in file time handling. Thanks [@safocl](https://github.com/safocl)!
|
||||
- Optimized memory allocation for depth-based rename processing.
|
||||
- Fixed Unicode normalization and non‑breaking space matching. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed date token replacements followed by capital letters. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
## PowerToys Run Plugins
|
||||
|
||||
- Fixed a plugin name typo and added Project Launcher to the third‑party list. Thanks [@artickc](https://github.com/artickc)!
|
||||
- Added the Open With Antigravity plugin to the third‑party list. Thanks [@artickc](https://github.com/artickc)!
|
||||
|
||||
## PowerToys Run
|
||||
|
||||
- Avoided unnecessary hotkey conflict checks when settings change.
|
||||
- Added QuickAI to the third-party PowerToys Run plugin list. Thanks [@ruslanlap](https://github.com/ruslanlap)!
|
||||
|
||||
## Quick Accent
|
||||
|
||||
- Added localized quotation marks to Quick Accent. Thanks [@warquys](https://github.com/warquys)!
|
||||
- Fixed duplicate and redundant characters in Quick Accent sets. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Fixed DPI positioning issues for Quick Accent on mixed-DPI setups. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
|
||||
## Settings
|
||||
|
||||
- Added a new tray icon that adapts to theme changes. Thanks [@HO-COOH](https://github.com/HO-COOH)!
|
||||
- Centralized module enable/disable logic for cleaner Settings UI updates.
|
||||
- Simplified Settings utilities by removing ISettingsUtils/ISettingsPath interfaces. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Improved Settings UI consistency and disabled-state visuals.
|
||||
- Added semantic headings to the Dashboard for better accessibility.
|
||||
- Introduced Quick Access as a standalone host with updated Settings integration.
|
||||
- Fixed Dashboard toggle flicker and sort menu checkmarks. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added Native AOT-compatible settings serialization.
|
||||
- Standardized mouse tool description text. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a global SettingsUtils singleton to reduce repeated initialization.
|
||||
|
||||
## Development
|
||||
|
||||
- Fixed broken devdocs links to the coding style guide. Thanks [@RubenFricke](https://github.com/RubenFricke)!
|
||||
- Migrated main and installer solutions to .slnx for improved build tooling.
|
||||
- Restored local installer builds after the WiX v5 upgrade with signing and versioning fixes.
|
||||
- Added incremental review tooling and structured AI prompts for PR/issue reviews.
|
||||
- Documented bot commands and cleaned up devdocs structure. Thanks [@noraa-junker](https://github.com/noraa-junker)!
|
||||
- Updated WinAppSDK pipeline defaults to 1.8 and fixed restore handling.
|
||||
- Updated the COMMUNITY list to reflect current roles.
|
||||
- Maintained community member ordering and added a new entry.
|
||||
- Re-enabled centralized PackageReference for native projects with VS auto-restore.
|
||||
- Disabled MSBuild caching by default in CI to avoid build instability.
|
||||
- Updated the latest WinAppSDK daily pipeline for split-dependency restores.
|
||||
- Suppressed experimental build warnings and aligned WrapPanel stretch handling.
|
||||
- Reordered the spell-check expect list for consistent automation.
|
||||
- Migrated native projects to centralized PackageReference management.
|
||||
- Cleaned spell-check dictionary entries and capitalization.
|
||||
- Synced commit/PR prompts and wired VS Code to repo prompt files.
|
||||
- Added VS Code build tasks and improved build script path handling.
|
||||
- Updated Windows App SDK package versions in central package management.
|
||||
- Migrated cmdpal extension native project to PackageReference and fixed outputs.
|
||||
- Reverted PackageReference changes back to packages.config where needed.
|
||||
- Bypassed a release version check for a failing DLL to keep pipelines green.
|
||||
- Consolidated Copilot instructions and fixed prompt frontmatter.
|
||||
- Added signing entries for new Quick Access binaries and CLI version metadata.
|
||||
- Fixed install scope detection to avoid mixed per-user/per-machine installs.
|
||||
- Added a Module Loader tool to quickly test PowerToys modules without full builds. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added update telemetry to understand auto-update checks and downloads.
|
||||
- Updated the telemetry package for new compliance requirements. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
|
||||
- Documented missing telemetry events in DATA_AND_PRIVACY.
|
||||
- Fixed UI test pipeline restores for .slnx solutions.
|
||||
- Added UI automation coverage for Advanced Paste clipboard history flows.
|
||||
- Stabilized FancyZones UI tests with more reliable selectors and screen recordings.
|
||||
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.1).
|
||||
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.98][github-next-release-work]!
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.99][github-next-release-work]!
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
|
||||
|
||||
BIN
doc/images/readme/Release-Banner.png
Normal file
BIN
doc/images/readme/Release-Banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
@@ -22,6 +22,16 @@
|
||||
|
||||
<ComponentGroup Id="DscResourcesComponentGroup">
|
||||
<ComponentRef Id="PowerToysDSCReference" />
|
||||
<?if $(var.PerUser) = "false" ?>
|
||||
<Component Id="SecureDSCModulesFolder" Guid="7D2F4E57-CCB2-4F89-9B8B-62E9B3CC4E12" Directory="DSCModulesReferenceFolder" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="SecureDSCModulesFolder" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<CreateFolder>
|
||||
<PermissionEx Sddl="D:PAI(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)(A;OICI;GRGX;;;BU)(A;OICIIO;GA;;;CO)" />
|
||||
</CreateFolder>
|
||||
</Component>
|
||||
<?endif?>
|
||||
<Component Id="RemoveDSCModulesFolder" Guid="A3C77D92-4E97-4C1A-9F2E-8B3C5D6E7F80" Directory="DSCModulesReferenceFolder">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemoveDSCModulesFolder" Value="" KeyPath="yes" />
|
||||
|
||||
@@ -300,10 +300,6 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT;
|
||||
}
|
||||
hstring Constants::ToggleKeyboardManagerActiveEvent()
|
||||
{
|
||||
return CommonSharedConstants::TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT;
|
||||
}
|
||||
hstring Constants::KeyboardManagerEngineInstanceMutex()
|
||||
{
|
||||
return CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX;
|
||||
|
||||
@@ -78,7 +78,6 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring MWBToggleEasyMouseEvent();
|
||||
static hstring MWBReconnectEvent();
|
||||
static hstring OpenNewKeyboardManagerEvent();
|
||||
static hstring ToggleKeyboardManagerActiveEvent();
|
||||
static hstring KeyboardManagerEngineInstanceMutex();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@ namespace PowerToys
|
||||
static String MWBToggleEasyMouseEvent();
|
||||
static String MWBReconnectEvent();
|
||||
static String OpenNewKeyboardManagerEvent();
|
||||
static String ToggleKeyboardManagerActiveEvent();
|
||||
static String KeyboardManagerEngineInstanceMutex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ namespace CommonSharedConstants
|
||||
const wchar_t ZOOMIT_BREAK_EVENT[] = L"Local\\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b";
|
||||
const wchar_t ZOOMIT_LIVEZOOM_EVENT[] = L"Local\\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d";
|
||||
const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30";
|
||||
const wchar_t ZOOMIT_SNIPOCR_EVENT[] = L"Local\\PowerToysZoomIt-SnipOcrEvent-a7c3b1d2-9e4f-4a6b-8d5c-1f2e3a4b5c6d";
|
||||
const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512";
|
||||
|
||||
// Path to the events used by PowerDisplay
|
||||
@@ -172,7 +173,6 @@ namespace CommonSharedConstants
|
||||
|
||||
// Path to events used by Keyboard Manager
|
||||
const wchar_t OPEN_NEW_KEYBOARD_MANAGER_EVENT[] = L"Local\\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f";
|
||||
const wchar_t TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT[] = L"Local\\PowerToysToggleKeyboardManagerActiveEvent-7f3a1d5c-2e94-4ff4-8b6a-90fd2bc4d2a7";
|
||||
const wchar_t KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX[] = L"Local\\PowerToys_KBMEngine_InstanceMutex";
|
||||
|
||||
// used from quick access window
|
||||
|
||||
@@ -16,7 +16,6 @@ using System.Threading.Tasks;
|
||||
using HostsUILib.Exceptions;
|
||||
using HostsUILib.Models;
|
||||
using HostsUILib.Settings;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
@@ -223,64 +222,17 @@ namespace HostsUILib.Helpers
|
||||
|
||||
public void OpenHostsFile()
|
||||
{
|
||||
var notepadFallback = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Try to open in default editor
|
||||
var key = Registry.ClassesRoot.OpenSubKey("SystemFileAssociations\\text\\shell\\edit\\command");
|
||||
if (key != null)
|
||||
{
|
||||
var commandPattern = key.GetValue(string.Empty).ToString(); // Default value
|
||||
var file = null as string;
|
||||
var args = null as string;
|
||||
|
||||
if (commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
|
||||
if (endQuoteIndex != -1)
|
||||
{
|
||||
file = commandPattern[1..endQuoteIndex];
|
||||
args = commandPattern[(endQuoteIndex + 1)..].Trim();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spaceIndex = commandPattern.IndexOf(' ');
|
||||
if (spaceIndex != -1)
|
||||
{
|
||||
file = commandPattern[..spaceIndex];
|
||||
args = commandPattern[(spaceIndex + 1)..].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (file != null && args != null)
|
||||
{
|
||||
args = args.Replace("%1", HostsFilePath);
|
||||
Process.Start(new ProcessStartInfo(file, args));
|
||||
}
|
||||
else
|
||||
{
|
||||
notepadFallback = true;
|
||||
}
|
||||
}
|
||||
var notepadPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Windows),
|
||||
"System32",
|
||||
"notepad.exe");
|
||||
Process.Start(new ProcessStartInfo(notepadPath, HostsFilePath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to open default editor", ex);
|
||||
notepadFallback = true;
|
||||
}
|
||||
|
||||
if (notepadFallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("notepad.exe", HostsFilePath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to open notepad", ex);
|
||||
}
|
||||
LoggerInstance.Logger.LogError("Failed to open notepad", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18013
src/modules/ZoomIt/ZoomIt/PanoramaCapture.cpp
Normal file
18013
src/modules/ZoomIt/ZoomIt/PanoramaCapture.cpp
Normal file
File diff suppressed because it is too large
Load Diff
42
src/modules/ZoomIt/ZoomIt/PanoramaCapture.h
Normal file
42
src/modules/ZoomIt/ZoomIt/PanoramaCapture.h
Normal file
@@ -0,0 +1,42 @@
|
||||
//============================================================================
|
||||
//
|
||||
// PanoramaCapture.h
|
||||
//
|
||||
// Panorama (scrolling) screen capture and stitching.
|
||||
//
|
||||
// Copyright (C) Mark Russinovich
|
||||
// Sysinternals - www.sysinternals.com
|
||||
//
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
//============================================================================
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <vector>
|
||||
|
||||
// Globals shared with the main ZoomIt module.
|
||||
extern bool g_PanoramaCaptureActive;
|
||||
extern bool g_PanoramaStopRequested;
|
||||
extern bool g_PanoramaDebugMode;
|
||||
|
||||
// Run the panorama capture flow: select a region, capture frames while
|
||||
// scrolling, stitch them together, and copy the result to the clipboard.
|
||||
bool RunPanoramaCaptureToClipboard( HWND hWnd );
|
||||
|
||||
// Run the panorama capture flow and save the result to a file via a
|
||||
// Save As dialog instead of copying to the clipboard.
|
||||
bool RunPanoramaCaptureToFile( HWND hWnd );
|
||||
|
||||
// Run a synthetic, non-interactive self-test for panorama frame stitching.
|
||||
// Returns true when stitching output matches expected dimensions/content.
|
||||
#ifdef _DEBUG
|
||||
bool RunPanoramaStitchSelfTest();
|
||||
|
||||
// Re-stitch frames from a specific debug dump directory.
|
||||
bool RunPanoramaStitchDumpDirectory( const wchar_t* path );
|
||||
|
||||
// Re-stitch accepted panorama frames from the latest debug dump session and
|
||||
// save output into that same session directory.
|
||||
bool RunPanoramaStitchLatestDebugDump();
|
||||
#endif
|
||||
@@ -11,6 +11,23 @@
|
||||
#include "Utility.h"
|
||||
#include "WindowsVersions.h"
|
||||
|
||||
static void SelectRectangleDebugLog( const wchar_t* format, ... )
|
||||
{
|
||||
#if _DEBUG
|
||||
wchar_t message[1024]{};
|
||||
va_list args;
|
||||
#pragma warning( push )
|
||||
#pragma warning( disable : 26492 )
|
||||
va_start( args, format );
|
||||
#pragma warning( pop )
|
||||
vswprintf_s( message, format, args );
|
||||
va_end( args );
|
||||
OutputDebugStringW( message );
|
||||
#else
|
||||
UNREFERENCED_PARAMETER( format );
|
||||
#endif
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// SelectRectangle::Start
|
||||
@@ -18,6 +35,12 @@
|
||||
//----------------------------------------------------------------------------
|
||||
bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
|
||||
{
|
||||
m_stopping = false;
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Start owner=%p fullMonitor=%d minSize=%d alpha=%u\n",
|
||||
ownerWindow,
|
||||
fullMonitor ? 1 : 0,
|
||||
MinSize(),
|
||||
Alpha() );
|
||||
WNDCLASSW windowClass{};
|
||||
windowClass.lpfnWndProc = []( HWND window, UINT message, WPARAM wordParam, LPARAM longParam ) -> LRESULT
|
||||
{
|
||||
@@ -46,10 +69,16 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
|
||||
|
||||
m_cancel = false;
|
||||
auto rect = GetMonitorRectFromCursor();
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Monitor rect=(%ld,%ld)-(%ld,%ld)\n",
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom );
|
||||
m_window = wil::unique_hwnd( CreateWindowExW( WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, m_className, nullptr, WS_POPUP,
|
||||
rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, ownerWindow,
|
||||
nullptr, nullptr, this ) );
|
||||
THROW_LAST_ERROR_IF_NULL( m_window.get() );
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Window created hwnd=%p\n", m_window.get() );
|
||||
|
||||
if( fullMonitor )
|
||||
{
|
||||
@@ -58,7 +87,11 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLayeredWindowAttributes( m_window.get(), 0, Alpha(), LWA_ALPHA );
|
||||
const BOOL layered = SetLayeredWindowAttributes( m_window.get(), 0, Alpha(), LWA_ALPHA );
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] SetLayeredWindowAttributes(alpha=%u) success=%d err=%lu\n",
|
||||
Alpha(),
|
||||
layered ? 1 : 0,
|
||||
layered ? 0 : GetLastError() );
|
||||
}
|
||||
|
||||
ShowWindow( m_window.get(), SW_SHOW );
|
||||
@@ -69,6 +102,7 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
|
||||
GetClipCursor( &m_oldClipRect );
|
||||
ClipCursor( &rect );
|
||||
m_setClip = true;
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Cursor clipped to monitor bounds\n" );
|
||||
}
|
||||
|
||||
MSG message;
|
||||
@@ -78,13 +112,20 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
|
||||
DispatchMessageW( &message );
|
||||
if( m_cancel )
|
||||
{
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Start cancelled via Stop()\n" );
|
||||
return false;
|
||||
}
|
||||
if( m_selected )
|
||||
{
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Selection finalized rect=(%ld,%ld)-(%ld,%ld)\n",
|
||||
m_selectedRect.left,
|
||||
m_selectedRect.top,
|
||||
m_selectedRect.right,
|
||||
m_selectedRect.bottom );
|
||||
break;
|
||||
}
|
||||
}
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Start complete selected=%d cancel=%d\n", m_selected ? 1 : 0, m_cancel ? 1 : 0 );
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -95,15 +136,38 @@ bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor )
|
||||
//----------------------------------------------------------------------------
|
||||
void SelectRectangle::Stop()
|
||||
{
|
||||
if( m_stopping )
|
||||
{
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Stop ignored due to reentrancy\n" );
|
||||
return;
|
||||
}
|
||||
|
||||
m_stopping = true;
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Stop hwnd=%p selected=%d cancel=%d clip=%d rect=(%ld,%ld)-(%ld,%ld)\n",
|
||||
m_window.get(),
|
||||
m_selected ? 1 : 0,
|
||||
m_cancel ? 1 : 0,
|
||||
m_setClip ? 1 : 0,
|
||||
m_selectedRect.left,
|
||||
m_selectedRect.top,
|
||||
m_selectedRect.right,
|
||||
m_selectedRect.bottom );
|
||||
if( m_setClip )
|
||||
{
|
||||
ClipCursor( &m_oldClipRect );
|
||||
m_setClip = false;
|
||||
}
|
||||
m_window.reset();
|
||||
|
||||
HWND window = m_window.release();
|
||||
if( window != nullptr && IsWindow( window ) )
|
||||
{
|
||||
DestroyWindow( window );
|
||||
}
|
||||
|
||||
m_selected = false;
|
||||
m_selectedRect = {};
|
||||
m_cancel = true;
|
||||
m_stopping = false;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
@@ -114,11 +178,20 @@ void SelectRectangle::Stop()
|
||||
void SelectRectangle::ShowSelected()
|
||||
{
|
||||
m_selected = true;
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] ShowSelected rect=(%ld,%ld)-(%ld,%ld) dpi=%u\n",
|
||||
m_selectedRect.left,
|
||||
m_selectedRect.top,
|
||||
m_selectedRect.right,
|
||||
m_selectedRect.bottom,
|
||||
m_dpi );
|
||||
|
||||
// Set the alpha to match the Windows graphics capture API yellow border
|
||||
// and set the window to be transparent and disabled, so it will be skipped
|
||||
// for hit testing and as a candidate for the next foreground window.
|
||||
SetLayeredWindowAttributes( m_window.get(), 0, 191, LWA_ALPHA );
|
||||
const BOOL layered = SetLayeredWindowAttributes( m_window.get(), 0, 191, LWA_ALPHA );
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] ShowSelected SetLayeredWindowAttributes(alpha=191) success=%d err=%lu\n",
|
||||
layered ? 1 : 0,
|
||||
layered ? 0 : GetLastError() );
|
||||
SetWindowLong( m_window.get(), GWL_EXSTYLE, GetWindowLong( m_window.get(), GWL_EXSTYLE ) | WS_EX_TRANSPARENT );
|
||||
EnableWindow( m_window.get(), FALSE );
|
||||
|
||||
@@ -144,6 +217,12 @@ void SelectRectangle::ShowSelected()
|
||||
point.x += windowRect.left;
|
||||
point.y += windowRect.top;
|
||||
MoveWindow( m_window.get(), point.x, point.y, rect.right, rect.bottom, true );
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Border window moved to (%ld,%ld) size=%ldx%ld borderWidth=%d\n",
|
||||
point.x,
|
||||
point.y,
|
||||
rect.right,
|
||||
rect.bottom,
|
||||
width );
|
||||
|
||||
// Use a region to keep everything but the border transparent.
|
||||
wil::unique_hrgn region{CreateRectRgnIndirect( &rect )};
|
||||
@@ -151,6 +230,11 @@ void SelectRectangle::ShowSelected()
|
||||
wil::unique_hrgn insideRegion{CreateRectRgnIndirect( &rect )};
|
||||
CombineRgn( region.get(), region.get(), insideRegion.get(), RGN_XOR );
|
||||
SetWindowRgn( m_window.get(), region.release(), true );
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Border window region applied\n" );
|
||||
|
||||
// Force immediate paint so the yellow border is visible instead of a
|
||||
// transient black frame from the class background brush.
|
||||
RedrawWindow( m_window.get(), nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_FRAME );
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
@@ -162,6 +246,7 @@ void SelectRectangle::UpdateOwner( HWND window )
|
||||
{
|
||||
if( m_window != nullptr )
|
||||
{
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] UpdateOwner hwnd=%p newOwner=%p\n", m_window.get(), window );
|
||||
SetWindowLongPtr( m_window.get(), GWLP_HWNDPARENT, reinterpret_cast<LONG_PTR>(window) );
|
||||
SetWindowPos( m_window.get(), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE );
|
||||
}
|
||||
@@ -179,10 +264,24 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
|
||||
case WM_CREATE:
|
||||
m_dpi = GetDpiForWindowHelper( window );
|
||||
SetWindowDisplayAffinity( window, WDA_EXCLUDEFROMCAPTURE );
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] WM_CREATE hwnd=%p dpi=%u\n", window, m_dpi );
|
||||
return 0;
|
||||
|
||||
case WM_DESTROY:
|
||||
Stop();
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] WM_DESTROY hwnd=%p\n", window );
|
||||
if( m_window.get() == window )
|
||||
{
|
||||
m_window.release();
|
||||
}
|
||||
if( m_setClip )
|
||||
{
|
||||
ClipCursor( &m_oldClipRect );
|
||||
m_setClip = false;
|
||||
}
|
||||
m_selected = false;
|
||||
m_selectedRect = {};
|
||||
m_cancel = true;
|
||||
m_stopping = false;
|
||||
return 0;
|
||||
|
||||
case WM_LBUTTONDOWN:
|
||||
@@ -190,6 +289,7 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
|
||||
SetCapture( window );
|
||||
|
||||
m_startPoint = { GET_X_LPARAM( longParam ), GET_Y_LPARAM( longParam ) };
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] WM_LBUTTONDOWN startPoint=(%ld,%ld)\n", m_startPoint.x, m_startPoint.y );
|
||||
[[fallthrough]];
|
||||
}
|
||||
case WM_MOUSEMOVE:
|
||||
@@ -199,6 +299,11 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
|
||||
GetClientRect( window, &rect );
|
||||
POINT point{ GET_X_LPARAM( longParam ), GET_Y_LPARAM( longParam ) };
|
||||
m_selectedRect = ForceRectInBounds( RectFromPointsMinSize( m_startPoint, point, MinSize() ), rect );
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] Drag rect=(%ld,%ld)-(%ld,%ld)\n",
|
||||
m_selectedRect.left,
|
||||
m_selectedRect.top,
|
||||
m_selectedRect.right,
|
||||
m_selectedRect.bottom );
|
||||
|
||||
// Use a region to carve out the selected rectangle.
|
||||
wil::unique_hrgn region{CreateRectRgnIndirect( &m_selectedRect )};
|
||||
@@ -211,6 +316,7 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
|
||||
case WM_KEYDOWN:
|
||||
if( wordParam == VK_ESCAPE )
|
||||
{
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] WM_KEYDOWN Escape pressed\n" );
|
||||
Stop();
|
||||
}
|
||||
return 0;
|
||||
@@ -218,12 +324,18 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
|
||||
case WM_KILLFOCUS:
|
||||
if( !m_selected )
|
||||
{
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] WM_KILLFOCUS before selection complete\n" );
|
||||
Stop();
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_LBUTTONUP:
|
||||
{
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] WM_LBUTTONUP selectedRect=(%ld,%ld)-(%ld,%ld)\n",
|
||||
m_selectedRect.left,
|
||||
m_selectedRect.top,
|
||||
m_selectedRect.right,
|
||||
m_selectedRect.bottom );
|
||||
if( m_setClip )
|
||||
{
|
||||
ClipCursor( &m_oldClipRect );
|
||||
@@ -249,6 +361,11 @@ LRESULT SelectRectangle::WindowProc( HWND window, UINT message, WPARAM wordParam
|
||||
|
||||
RECT rect;
|
||||
GetClientRect( window, &rect );
|
||||
SelectRectangleDebugLog( L"[SelectRectangle] WM_PAINT selected border rect=(%ld,%ld)-(%ld,%ld)\n",
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom );
|
||||
|
||||
// Draw a border matching the Windows graphics capture API border.
|
||||
// The outer frame is yellow and two logical pixels wide, while the
|
||||
|
||||
@@ -20,10 +20,14 @@ public:
|
||||
void MinSize( int minSize ) { m_minSize = minSize; }
|
||||
int MinSize() const { return m_minSize; }
|
||||
RECT SelectedRect() const { return m_selectedRect; }
|
||||
bool IsActive() const { return m_window != nullptr; }
|
||||
|
||||
bool Start( HWND ownerWindow = nullptr, bool fullMonitor = false );
|
||||
void Stop();
|
||||
void UpdateOwner( HWND window );
|
||||
void Hide() { if( m_window ) ShowWindow( m_window.get(), SW_HIDE ); }
|
||||
void Show() { if( m_window ) ShowWindow( m_window.get(), SW_SHOWNA ); }
|
||||
void SetExcludeFromCapture( bool exclude ) { if( m_window ) SetWindowDisplayAffinity( m_window.get(), exclude ? WDA_EXCLUDEFROMCAPTURE : WDA_NONE ); }
|
||||
|
||||
private:
|
||||
BYTE m_alpha = 176;
|
||||
@@ -36,6 +40,7 @@ private:
|
||||
RECT m_oldClipRect{};
|
||||
bool m_selected{ false };
|
||||
bool m_setClip{ false };
|
||||
bool m_stopping{ false };
|
||||
POINT m_startPoint{};
|
||||
wil::unique_hwnd m_window;
|
||||
|
||||
|
||||
@@ -406,7 +406,10 @@ static bool LoadGifFrames(const std::wstring& gifPath, VideoRecordingSession::Tr
|
||||
|
||||
const auto& lastFrame = pData->gifFrames.back();
|
||||
pData->videoDuration = winrt::TimeSpan{ lastFrame.start.count() + lastFrame.duration.count() };
|
||||
pData->trimEnd = pData->videoDuration;
|
||||
if( pData->trimEnd.count() <= 0 )
|
||||
{
|
||||
pData->trimEnd = pData->videoDuration;
|
||||
}
|
||||
pData->gifFramesLoaded = true;
|
||||
pData->gifLastFrameIndex = 0;
|
||||
|
||||
@@ -721,13 +724,9 @@ namespace
|
||||
SetDlgItemText(hDlg, IDC_TRIM_DURATION_LABEL, durationText.c_str());
|
||||
}
|
||||
|
||||
// Enable OK when trimming is active (even if unchanged since dialog opened),
|
||||
// or when the user changed the selection (including reverting to full length).
|
||||
const bool trimChanged = (pData->trimStart.count() != pData->originalTrimStart.count()) ||
|
||||
(pData->trimEnd.count() != pData->originalTrimEnd.count());
|
||||
const bool trimIsActive = (pData->trimStart.count() > 0) ||
|
||||
(pData->videoDuration.count() > 0 && pData->trimEnd.count() < pData->videoDuration.count());
|
||||
EnableWindow(GetDlgItem(hDlg, IDOK), trimChanged || trimIsActive);
|
||||
// Always enable OK so users can close the dialog after previewing
|
||||
// without being forced to use Cancel.
|
||||
EnableWindow(GetDlgItem(hDlg, IDOK), TRUE);
|
||||
}
|
||||
|
||||
RECT GetTimelineTrackRect(const RECT& clientRect, UINT dpi)
|
||||
@@ -1345,7 +1344,10 @@ public:
|
||||
auto trimResult = VideoRecordingSession::ShowTrimDialog(hParent, m_videoPath, *m_pTrimStart, *m_pTrimEnd);
|
||||
if (trimResult == IDOK)
|
||||
{
|
||||
*m_pShouldTrim = true;
|
||||
// Trim values are only written back when the user actually
|
||||
// changed the selection, so a non-zero trimEnd means a
|
||||
// real trim is requested.
|
||||
*m_pShouldTrim = (m_pTrimEnd->count() > 0);
|
||||
}
|
||||
else if( trimResult == IDCANCEL )
|
||||
{
|
||||
@@ -1502,12 +1504,13 @@ INT_PTR VideoRecordingSession::ShowTrimDialog(
|
||||
HWND hParent,
|
||||
const std::wstring& videoPath,
|
||||
winrt::TimeSpan& trimStart,
|
||||
winrt::TimeSpan& trimEnd)
|
||||
winrt::TimeSpan& trimEnd,
|
||||
bool standaloneMode)
|
||||
{
|
||||
std::promise<INT_PTR> resultPromise;
|
||||
auto resultFuture = resultPromise.get_future();
|
||||
|
||||
std::thread staThread([hParent, videoPath, &trimStart, &trimEnd, promise = std::move(resultPromise)]() mutable
|
||||
std::thread staThread([hParent, videoPath, &trimStart, &trimEnd, standaloneMode, promise = std::move(resultPromise)]() mutable
|
||||
{
|
||||
bool coInitialized = false;
|
||||
try
|
||||
@@ -1525,7 +1528,7 @@ INT_PTR VideoRecordingSession::ShowTrimDialog(
|
||||
|
||||
try
|
||||
{
|
||||
INT_PTR dlgResult = ShowTrimDialogInternal(hParent, videoPath, trimStart, trimEnd);
|
||||
INT_PTR dlgResult = ShowTrimDialogInternal(hParent, videoPath, trimStart, trimEnd, standaloneMode);
|
||||
promise.set_value(dlgResult);
|
||||
}
|
||||
catch (const winrt::hresult_error& e)
|
||||
@@ -1584,7 +1587,8 @@ INT_PTR VideoRecordingSession::ShowTrimDialogInternal(
|
||||
HWND hParent,
|
||||
const std::wstring& videoPath,
|
||||
winrt::TimeSpan& trimStart,
|
||||
winrt::TimeSpan& trimEnd)
|
||||
winrt::TimeSpan& trimEnd,
|
||||
bool standaloneMode)
|
||||
{
|
||||
TrimDialogData data;
|
||||
data.videoPath = videoPath;
|
||||
@@ -1592,6 +1596,7 @@ INT_PTR VideoRecordingSession::ShowTrimDialogInternal(
|
||||
data.trimStart = trimStart;
|
||||
data.trimEnd = trimEnd;
|
||||
data.isGif = IsGifPath(videoPath);
|
||||
data.standaloneMode = standaloneMode;
|
||||
|
||||
if (data.isGif)
|
||||
{
|
||||
@@ -1786,8 +1791,17 @@ INT_PTR VideoRecordingSession::ShowTrimDialogInternal(
|
||||
|
||||
if (result == IDOK)
|
||||
{
|
||||
trimStart = data.trimStart;
|
||||
trimEnd = data.trimEnd;
|
||||
// Only write back trim values when the user actually changed the
|
||||
// selection. This lets the caller distinguish "confirmed without
|
||||
// trimming" (preview-only) from a real trim operation.
|
||||
const bool selectionChanged =
|
||||
(data.trimStart.count() != data.originalTrimStart.count()) ||
|
||||
(data.trimEnd.count() != data.originalTrimEnd.count());
|
||||
if (selectionChanged)
|
||||
{
|
||||
trimStart = data.trimStart;
|
||||
trimEnd = data.trimEnd;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -3890,6 +3904,12 @@ INT_PTR CALLBACK VideoRecordingSession::TrimDialogProc(HWND hDlg, UINT message,
|
||||
// Make OK the default button
|
||||
SendMessage(hDlg, DM_SETDEFID, IDOK, 0);
|
||||
|
||||
// In standalone mode, change OK button text to "Save As"
|
||||
if (pData->standaloneMode)
|
||||
{
|
||||
SetDlgItemText(hDlg, IDOK, L"Save As");
|
||||
}
|
||||
|
||||
// Subclass the dialog to handle resize grip hit testing
|
||||
SetWindowSubclass(hDlg, TrimDialogSubclassProc, 0, reinterpret_cast<DWORD_PTR>(pData));
|
||||
|
||||
@@ -5029,6 +5049,115 @@ INT_PTR CALLBACK VideoRecordingSession::TrimDialogProc(HWND hDlg, UINT message,
|
||||
case IDOK:
|
||||
pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
|
||||
StopPlayback(hDlg, pData);
|
||||
|
||||
if (pData->standaloneMode)
|
||||
{
|
||||
// In standalone mode, "Save As" shows a save dialog and performs the trim
|
||||
auto saveDialog = wil::CoCreateInstance<::IFileSaveDialog>(CLSID_FileSaveDialog);
|
||||
|
||||
FILEOPENDIALOGOPTIONS options;
|
||||
if (SUCCEEDED(saveDialog->GetOptions(&options)))
|
||||
saveDialog->SetOptions(options | FOS_FORCEFILESYSTEM);
|
||||
|
||||
wil::com_ptr<::IShellItem> videosItem;
|
||||
if (SUCCEEDED(SHGetKnownFolderItem(FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr,
|
||||
IID_IShellItem, (void**)videosItem.put())))
|
||||
saveDialog->SetDefaultFolder(videosItem.get());
|
||||
|
||||
// Derive suggested filename from source
|
||||
std::wstring suggestedName;
|
||||
{
|
||||
auto pos = pData->videoPath.find_last_of(L"\\/");
|
||||
suggestedName = (pos != std::wstring::npos) ? pData->videoPath.substr(pos + 1) : pData->videoPath;
|
||||
auto dot = suggestedName.find_last_of(L'.');
|
||||
if (dot != std::wstring::npos)
|
||||
suggestedName.insert(dot, L"_trimmed");
|
||||
else
|
||||
suggestedName += L"_trimmed";
|
||||
}
|
||||
|
||||
if (pData->isGif)
|
||||
{
|
||||
saveDialog->SetDefaultExtension(L".gif");
|
||||
COMDLG_FILTERSPEC fileTypes[] = { { L"GIF Animation", L"*.gif" } };
|
||||
saveDialog->SetFileTypes(_countof(fileTypes), fileTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
saveDialog->SetDefaultExtension(L".mp4");
|
||||
COMDLG_FILTERSPEC fileTypes[] = { { L"MP4 Video", L"*.mp4" } };
|
||||
saveDialog->SetFileTypes(_countof(fileTypes), fileTypes);
|
||||
}
|
||||
saveDialog->SetFileName(suggestedName.c_str());
|
||||
saveDialog->SetTitle(L"ZoomIt: Save Trimmed Video As...");
|
||||
|
||||
HRESULT hr = saveDialog->Show(hDlg);
|
||||
if (FAILED(hr))
|
||||
{
|
||||
// User cancelled save dialog — return to trim editor
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
wil::com_ptr<::IShellItem> resultItem;
|
||||
THROW_IF_FAILED(saveDialog->GetResult(resultItem.put()));
|
||||
wil::unique_cotaskmem_string savePath;
|
||||
THROW_IF_FAILED(resultItem->GetDisplayName(SIGDN_FILESYSPATH, savePath.put()));
|
||||
|
||||
// Capture what we need before closing the dialog
|
||||
std::wstring videoPath = pData->videoPath;
|
||||
bool isGif = pData->isGif;
|
||||
auto trimStart = pData->trimStart;
|
||||
auto trimEnd = pData->trimEnd;
|
||||
std::wstring savePathStr(savePath.get());
|
||||
|
||||
// Close the trim dialog immediately
|
||||
EndDialog(hDlg, IDOK);
|
||||
|
||||
// Perform the trim after the dialog is closed
|
||||
try
|
||||
{
|
||||
auto trimOp = isGif
|
||||
? TrimGifAsync(videoPath, trimStart, trimEnd)
|
||||
: TrimVideoAsync(videoPath, trimStart, trimEnd);
|
||||
|
||||
// Pump messages while waiting for async operation
|
||||
while (trimOp.Status() == winrt::AsyncStatus::Started)
|
||||
{
|
||||
MSG msg;
|
||||
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
Sleep(10);
|
||||
}
|
||||
|
||||
auto trimmedPath = std::wstring(trimOp.GetResults());
|
||||
if (trimmedPath.empty())
|
||||
{
|
||||
MessageBox(nullptr, L"Failed to trim video.", L"Error", MB_OK | MB_ICONERROR);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Copy trimmed file to the user-chosen save location
|
||||
if (!CopyFile(trimmedPath.c_str(), savePathStr.c_str(), FALSE))
|
||||
{
|
||||
MessageBox(nullptr, L"Failed to save the trimmed file.", L"Error", MB_OK | MB_ICONERROR);
|
||||
DeleteFile(trimmedPath.c_str());
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Clean up temp file
|
||||
DeleteFile(trimmedPath.c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
MessageBox(nullptr, L"Failed to trim video.", L"Error", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Trim times are already set by mouse dragging
|
||||
EndDialog(hDlg, IDOK);
|
||||
return TRUE;
|
||||
|
||||
@@ -132,6 +132,7 @@ public:
|
||||
bool isDragging{ false };
|
||||
int lastPlayheadX{ -1 }; // Track last playhead pixel position for efficient invalidation
|
||||
MMRESULT mmTimerId{ 0 }; // Multimedia timer for smooth MP4 playback
|
||||
bool standaloneMode{ false }; // When true, OK becomes "Save As" and handles file saving directly
|
||||
|
||||
// Helper to convert time to pixel position
|
||||
int TimeToPixel(winrt::Windows::Foundation::TimeSpan time, int timelineWidth) const
|
||||
@@ -162,7 +163,8 @@ public:
|
||||
HWND hParent,
|
||||
const std::wstring& videoPath,
|
||||
winrt::Windows::Foundation::TimeSpan& trimStart,
|
||||
winrt::Windows::Foundation::TimeSpan& trimEnd);
|
||||
winrt::Windows::Foundation::TimeSpan& trimEnd,
|
||||
bool standaloneMode = false);
|
||||
|
||||
private:
|
||||
static INT_PTR CALLBACK TrimDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
@@ -179,7 +181,8 @@ private:
|
||||
HWND hParent,
|
||||
const std::wstring& videoPath,
|
||||
winrt::Windows::Foundation::TimeSpan& trimStart,
|
||||
winrt::Windows::Foundation::TimeSpan& trimEnd);
|
||||
winrt::Windows::Foundation::TimeSpan& trimEnd,
|
||||
bool standaloneMode = false);
|
||||
|
||||
private:
|
||||
VideoRecordingSession(
|
||||
|
||||
@@ -113,26 +113,26 @@ END
|
||||
// Dialog
|
||||
//
|
||||
|
||||
OPTIONS DIALOGEX 0, 0, 299, 325
|
||||
OPTIONS DIALOGEX 0, 0, 299, 331
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CLIPSIBLINGS | WS_CAPTION | WS_SYSMENU
|
||||
EXSTYLE WS_EX_CONTROLPARENT
|
||||
CAPTION "ZoomIt - Sysinternals: www.sysinternals.com"
|
||||
FONT 8, "MS Shell Dlg", 0, 0, 0x0
|
||||
BEGIN
|
||||
DEFPUSHBUTTON "OK",IDOK,186,306,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,243,306,50,14
|
||||
LTEXT "ZoomIt v10.1",IDC_VERSION,42,7,73,10
|
||||
DEFPUSHBUTTON "OK",IDOK,184,308,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,241,308,50,14
|
||||
LTEXT "ZoomIt v11.0",IDC_VERSION,42,7,73,10
|
||||
LTEXT "Copyright \251 2006-2026 Mark Russinovich",IDC_COPYRIGHT,42,17,251,8
|
||||
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
||||
"SysLink",WS_TABSTOP,42,26,150,9
|
||||
ICON "APPICON",IDC_STATIC,12,9,20,20
|
||||
CONTROL "Show tray icon",IDC_SHOW_TRAY_ICON,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,295,105,10
|
||||
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,46,285,247
|
||||
CONTROL "Run ZoomIt when Windows starts",IDC_AUTOSTART,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,309,122,10
|
||||
CONTROL "Show tray icon",IDC_SHOW_TRAY_ICON,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,302,105,10
|
||||
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,45,285,255
|
||||
CONTROL "Run ZoomIt when Windows starts",IDC_AUTOSTART,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,316,122,10
|
||||
END
|
||||
|
||||
ADVANCED_BREAK DIALOGEX 0, 0, 209, 225
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||
ADVANCED_BREAK DIALOGEX 0, 0, 209, 223
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||
CAPTION "Advanced Break Options"
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
@@ -158,8 +158,8 @@ BEGIN
|
||||
EDITTEXT IDC_BACKGROUND_FILE,62,164,125,12,ES_AUTOHSCROLL | ES_READONLY
|
||||
PUSHBUTTON "&...",IDC_BACKGROUND_BROWSE,188,164,13,11
|
||||
CONTROL "Scale to screen:",IDC_CHECK_BACKGROUND_STRETCH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,58,180,67,10,WS_EX_RIGHT
|
||||
DEFPUSHBUTTON "OK",IDOK,97,199,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,150,199,50,14
|
||||
DEFPUSHBUTTON "OK",IDOK,97,202,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,150,202,50,14
|
||||
LTEXT "Alarm Sound File:",IDC_STATIC_SOUND_FILE,61,26,56,8
|
||||
LTEXT "Timer Opacity:",IDC_STATIC,8,59,48,8
|
||||
LTEXT "Timer Position:",IDC_STATIC,8,77,48,8
|
||||
@@ -215,21 +215,23 @@ BEGIN
|
||||
GROUPBOX "Sample",IDC_TEXT_FONT,8,61,99,28
|
||||
END
|
||||
|
||||
BREAK DIALOGEX 0, 0, 260, 123
|
||||
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||
BREAK DIALOGEX 0, 0, 260, 159
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_BREAK_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,52,67,80,12
|
||||
EDITTEXT IDC_TIMER,52,86,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
|
||||
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,66,86,11,12
|
||||
LTEXT "minutes",IDC_STATIC,88,88,25,8
|
||||
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,192,102,41,14
|
||||
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,230,33
|
||||
LTEXT "Start Timer:",IDC_STATIC,7,70,39,8
|
||||
LTEXT "Timer:",IDC_STATIC,7,88,20,8
|
||||
LTEXT "Change the break timer color using the same keys that the drawing color. The break timer font is the same as text font.",IDC_STATIC,7,45,230,20
|
||||
CONTROL "",IDC_BREAK_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,52,74,80,12
|
||||
EDITTEXT IDC_TIMER,52,93,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
|
||||
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,66,93,11,12
|
||||
LTEXT "minutes",IDC_STATIC,88,95,25,8
|
||||
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,213,140,41,14
|
||||
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,242,33
|
||||
LTEXT "Start Timer:",IDC_STATIC,7,77,39,8
|
||||
LTEXT "Timer:",IDC_STATIC,7,95,20,8
|
||||
LTEXT "Change the break timer color using the same keys that the drawing color, including background color. The break timer font is the same as text font.",IDC_STATIC,7,45,241,26
|
||||
CONTROL "Show Time Elapsed After Expiration:",IDC_CHECK_SHOW_EXPIRED,
|
||||
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,8,104,132,10
|
||||
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,111,130,10
|
||||
CONTROL "Lock Workstation During Break:",IDC_CHECK_LOCK_WORKSTATION,
|
||||
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,126,113,10
|
||||
END
|
||||
|
||||
1543 DIALOGEX 100, 50, 216, 131
|
||||
@@ -249,19 +251,19 @@ BEGIN
|
||||
CTEXT "AaBbYyZz",1092,16,88,127,31,SS_NOPREFIX | NOT WS_VISIBLE
|
||||
END
|
||||
|
||||
LIVEZOOM DIALOGEX 0, 0, 260, 134
|
||||
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||
LIVEZOOM DIALOGEX 0, 0, 317, 136
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_LIVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,69,108,80,12
|
||||
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,230,18
|
||||
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,255,18
|
||||
LTEXT "LiveZoom Toggle:",IDC_STATIC,7,110,62,8
|
||||
LTEXT "To enter and exit LiveZoom, enter the hotkey specified below.",IDC_STATIC,7,94,230,13
|
||||
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,230,27
|
||||
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,230,32
|
||||
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,249,32
|
||||
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,255,27
|
||||
END
|
||||
|
||||
RECORD DIALOGEX 0, 0, 260, 181
|
||||
RECORD DIALOGEX 0, 0, 263, 224
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
@@ -282,19 +284,33 @@ BEGIN
|
||||
CONTROL "Mono",IDC_MIC_MONO_MIX,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,98,161,30,10
|
||||
COMBOBOX IDC_MICROPHONE,81,176,152,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
|
||||
LTEXT "Microphone:",IDC_MICROPHONE_LABEL,32,178,47,8
|
||||
PUSHBUTTON "&Trim",IDC_TRIM_FILE,207,209,53,14
|
||||
END
|
||||
|
||||
SNIP DIALOGEX 0, 0, 260, 68
|
||||
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
SNIP DIALOGEX 0, 0, 260, 80
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,55,32,80,12
|
||||
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file.",IDC_STATIC,7,7,230,19
|
||||
LTEXT "Snip Toggle:",IDC_STATIC,7,33,45,8
|
||||
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file. ",IDC_STATIC,7,7,230,19
|
||||
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,67,32,80,12
|
||||
LTEXT "Copy text from the selected region to the clipboard:",IDC_STATIC,7,50,230,10
|
||||
LTEXT "Text Toggle:",IDC_STATIC,7,65,55,8
|
||||
CONTROL "",IDC_SNIP_OCR_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,67,63,80,12
|
||||
END
|
||||
|
||||
PANORAMA DIALOGEX 0, 0, 260, 105
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
LTEXT "Capture a scrolling panorama of a selected screen region. Select the area, then scroll the content. Move slowly and consistently, and do not rewind to previously covered areas. Press the hotkey again or with Shift to save to a file.",IDC_STATIC,7,7,245,33
|
||||
LTEXT "Panorama Toggle:",IDC_STATIC,7,74,63,8
|
||||
CONTROL "",IDC_SNIP_PANORAMA_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,72,80,12
|
||||
LTEXT "For the best results, scroll slowly and at a constant rate, do not include stationary content (like scrollbars) in the capture area, and avoid content that is changing (e.g., animations or videos). ",IDC_STATIC,7,41,245,30
|
||||
END
|
||||
|
||||
DEMOTYPE DIALOGEX 0, 0, 260, 249
|
||||
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
|
||||
FONT 8, "MS Shell Dlg", 400, 0, 0x1
|
||||
BEGIN
|
||||
CONTROL "",IDC_DEMOTYPE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,74,154,80,12
|
||||
@@ -308,11 +324,11 @@ BEGIN
|
||||
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
|
||||
EDITTEXT IDC_DEMOTYPE_FILE,44,137,167,12,ES_AUTOHSCROLL | ES_READONLY
|
||||
LTEXT "Input file:",IDC_STATIC,7,139,32,8
|
||||
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,230,24
|
||||
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,230,24
|
||||
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,249,24
|
||||
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,247,24
|
||||
LTEXT " - Insert pauses with the [pause:n] keyword where 'n' is seconds. ",IDC_STATIC,19,34,218,11
|
||||
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,230,16
|
||||
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,230,16
|
||||
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,245,16
|
||||
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,243,16
|
||||
LTEXT "- Send text via the clipboard with [paste] and [/paste]. ",IDC_STATIC,23,45,210,8
|
||||
LTEXT "- Send keystrokes with [enter], [up], [down], [left], and [right].",IDC_STATIC,23,56,210,8
|
||||
END
|
||||
@@ -349,13 +365,13 @@ BEGIN
|
||||
"OPTIONS", DIALOG
|
||||
BEGIN
|
||||
RIGHTMARGIN, 293
|
||||
BOTTOMMARGIN, 320
|
||||
BOTTOMMARGIN, 326
|
||||
END
|
||||
|
||||
"ADVANCED_BREAK", DIALOG
|
||||
BEGIN
|
||||
RIGHTMARGIN, 207
|
||||
BOTTOMMARGIN, 215
|
||||
BOTTOMMARGIN, 214
|
||||
END
|
||||
|
||||
"ZOOM", DIALOG
|
||||
@@ -383,7 +399,7 @@ BEGIN
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 116
|
||||
BOTTOMMARGIN, 154
|
||||
END
|
||||
|
||||
1543, DIALOG
|
||||
@@ -395,22 +411,27 @@ BEGIN
|
||||
"LIVEZOOM", DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 181
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 127
|
||||
BOTTOMMARGIN, 89
|
||||
END
|
||||
|
||||
"RECORD", DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
RIGHTMARGIN, 260
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 164
|
||||
BOTTOMMARGIN, 223
|
||||
END
|
||||
|
||||
"SNIP", DIALOG
|
||||
BEGIN
|
||||
LEFTMARGIN, 7
|
||||
TOPMARGIN, 7
|
||||
BOTTOMMARGIN, 61
|
||||
END
|
||||
|
||||
"PANORAMA", DIALOG
|
||||
BEGIN
|
||||
END
|
||||
|
||||
"DEMOTYPE", DIALOG
|
||||
@@ -496,6 +517,16 @@ BEGIN
|
||||
0
|
||||
END
|
||||
|
||||
ADVANCED_BREAK AFX_DIALOG_LAYOUT
|
||||
BEGIN
|
||||
0
|
||||
END
|
||||
|
||||
PANORAMA AFX_DIALOG_LAYOUT
|
||||
BEGIN
|
||||
0
|
||||
END
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<DisableSpecificWarnings>4100;4091;4245</DisableSpecificWarnings>
|
||||
<AdditionalIncludeDirectories>..\..\..\;$(MSBuildThisFileDirectory)..\..\..\common\sysinternals;%(AdditionalIncludeDirectories);</AdditionalIncludeDirectories>
|
||||
<DisableSpecificWarnings>26451;4100;4091;4245</DisableSpecificWarnings>
|
||||
<AdditionalIncludeDirectories>..\..\..\;$(MSBuildThisFileDirectory)..\..\..\common\sysinternals;..\ZoomItBreak;$(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
@@ -90,7 +90,7 @@
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(InterPlatformDir)</AdditionalIncludeDirectories>
|
||||
</ResourceCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<RandomizedBaseAddress>true</RandomizedBaseAddress>
|
||||
@@ -109,10 +109,10 @@
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;_M_X64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Culture>0x0409</Culture>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(MSBuildThisFileDirectory)..\ZoomItBreak\$(Platform)\$(Configuration)\</AdditionalIncludeDirectories>
|
||||
</ResourceCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<RandomizedBaseAddress>true</RandomizedBaseAddress>
|
||||
@@ -132,10 +132,10 @@
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;_M_ARM64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Culture>0x0409</Culture>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(MSBuildThisFileDirectory)..\ZoomItBreak\$(Platform)\$(Configuration)\</AdditionalIncludeDirectories>
|
||||
</ResourceCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<FixedBaseAddress>
|
||||
@@ -156,7 +156,7 @@
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(InterPlatformDir)</AdditionalIncludeDirectories>
|
||||
</ResourceCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<RandomizedBaseAddress>false</RandomizedBaseAddress>
|
||||
@@ -174,10 +174,10 @@
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_M_X64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Culture>0x0409</Culture>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(MSBuildThisFileDirectory)..\ZoomItBreak\$(Platform)\$(Configuration)\</AdditionalIncludeDirectories>
|
||||
</ResourceCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<UACUIAccess>true</UACUIAccess>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -196,10 +196,10 @@
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_M_ARM64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Culture>0x0409</Culture>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)..\..\..\common\version;$(MSBuildThisFileDirectory)PowerToys;$(MSBuildThisFileDirectory)..\ZoomItBreak\$(Platform)\$(Configuration)\</AdditionalIncludeDirectories>
|
||||
</ResourceCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>Shlwapi.lib;comctl32.lib;odbc32.lib;odbccp32.lib;version.lib;Winmm.lib;gdiplus.lib;Msimg32.lib;Wtsapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<UACUIAccess>true</UACUIAccess>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -208,6 +208,14 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\ZoomItBreak\BreakTimer.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="AudioSampleGenerator.cpp">
|
||||
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</MultiProcessorCompilation>
|
||||
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</MultiProcessorCompilation>
|
||||
@@ -249,6 +257,14 @@
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="GifRecordingSession.cpp" />
|
||||
<ClCompile Include="PanoramaCapture.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="pch.cpp" />
|
||||
<ClCompile Include="SelectRectangle.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
|
||||
@@ -300,11 +316,13 @@
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\ZoomItBreak\BreakTimer.h" />
|
||||
<ClInclude Include="AudioSampleGenerator.h" />
|
||||
<ClInclude Include="LoopbackCapture.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
|
||||
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
|
||||
<ClInclude Include="GifRecordingSession.h" />
|
||||
<ClInclude Include="PanoramaCapture.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="Registry.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
@@ -378,4 +396,4 @@
|
||||
<Import Project="..\..\..\..\packages\robmikh.common.0.0.23-beta\build\native\robmikh.common.targets" Condition="Exists('..\..\..\..\packages\robmikh.common.0.0.23-beta\build\native\robmikh.common.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -60,6 +60,12 @@
|
||||
<ClCompile Include="GifRecordingSession.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PanoramaCapture.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\ZoomItBreak\BreakTimer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Registry.h">
|
||||
@@ -107,6 +113,12 @@
|
||||
<ClInclude Include="GifRecordingSession.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="PanoramaCapture.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\ZoomItBreak\BreakTimer.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="appicon.ico">
|
||||
|
||||
@@ -17,6 +17,8 @@ DWORD g_BreakToggleKey = ((HOTKEYF_CONTROL) << 8)| '3';
|
||||
DWORD g_DemoTypeToggleKey = ((HOTKEYF_CONTROL) << 8) | '7';
|
||||
DWORD g_RecordToggleKey = ((HOTKEYF_CONTROL) << 8) | '5';
|
||||
DWORD g_SnipToggleKey = ((HOTKEYF_CONTROL) << 8) | '6';
|
||||
DWORD g_SnipPanoramaToggleKey = ((HOTKEYF_CONTROL) << 8) | '8';
|
||||
DWORD g_SnipOcrToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_ALT) << 8) | '6';
|
||||
|
||||
DWORD g_ShowExpiredTime = 1;
|
||||
DWORD g_SliderZoomLevel = 3;
|
||||
@@ -24,6 +26,7 @@ BOOLEAN g_AnimateZoom = TRUE;
|
||||
BOOLEAN g_SmoothImage = TRUE;
|
||||
DWORD g_PenColor = COLOR_RED;
|
||||
DWORD g_BreakPenColor = COLOR_RED;
|
||||
DWORD g_BreakBackgroundColor = 0;
|
||||
DWORD g_RootPenWidth = PEN_WIDTH;
|
||||
int g_FontScale = 10;
|
||||
DWORD g_BreakTimeout = 10;
|
||||
@@ -40,6 +43,7 @@ BOOLEAN g_ShowTrayIcon = TRUE;
|
||||
BOOLEAN g_SnapToGrid = TRUE;
|
||||
BOOLEAN g_TelescopeZoomOut = TRUE;
|
||||
BOOLEAN g_BreakOnSecondary = FALSE;
|
||||
BOOLEAN g_BreakLockWorkstation = FALSE;
|
||||
LOGFONT g_LogFont;
|
||||
BOOLEAN g_DemoTypeUserDriven = false;
|
||||
TCHAR g_DemoTypeFile[MAX_PATH] = {0};
|
||||
@@ -66,10 +70,13 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"DrawToggleKey", SETTING_TYPE_DWORD, 0, &g_DrawToggleKey, static_cast<DOUBLE>(g_DrawToggleKey) },
|
||||
{ L"RecordToggleKey", SETTING_TYPE_DWORD, 0, &g_RecordToggleKey, static_cast<DOUBLE>(g_RecordToggleKey) },
|
||||
{ L"SnipToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipToggleKey, static_cast<DOUBLE>(g_SnipToggleKey) },
|
||||
{ L"SnipPanoramaToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipPanoramaToggleKey, static_cast<DOUBLE>(g_SnipPanoramaToggleKey) },
|
||||
{ L"SnipOcrToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipOcrToggleKey, static_cast<DOUBLE>(g_SnipOcrToggleKey) },
|
||||
{ L"PenColor", SETTING_TYPE_DWORD, 0, &g_PenColor, static_cast<DOUBLE>(g_PenColor) },
|
||||
{ L"PenWidth", SETTING_TYPE_DWORD, 0, &g_RootPenWidth, static_cast<DOUBLE>(g_RootPenWidth) },
|
||||
{ L"OptionsShown", SETTING_TYPE_BOOLEAN, 0, &g_OptionsShown, static_cast<DOUBLE>(g_OptionsShown) },
|
||||
{ L"BreakPenColor", SETTING_TYPE_DWORD, 0, &g_BreakPenColor, static_cast<DOUBLE>(g_BreakPenColor) },
|
||||
{ L"BreakBackgroundColor", SETTING_TYPE_DWORD, 0, &g_BreakBackgroundColor, static_cast<DOUBLE>(g_BreakBackgroundColor) },
|
||||
{ L"BreakTimerKey", SETTING_TYPE_DWORD, 0, &g_BreakToggleKey, static_cast<DOUBLE>(g_BreakToggleKey) },
|
||||
{ L"DemoTypeToggleKey", SETTING_TYPE_DWORD, 0, &g_DemoTypeToggleKey, static_cast<DOUBLE>(g_DemoTypeToggleKey) },
|
||||
{ L"DemoTypeFile", SETTING_TYPE_STRING, sizeof( g_DemoTypeFile ), g_DemoTypeFile, static_cast<DOUBLE>(0) },
|
||||
@@ -85,6 +92,7 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"BreakTimerPosition", SETTING_TYPE_DWORD, 0, &g_BreakTimerPosition, static_cast<DOUBLE>(g_BreakTimerPosition) },
|
||||
{ L"BreakShowDesktop", SETTING_TYPE_BOOLEAN, 0, &g_BreakShowDesktop, static_cast<DOUBLE>(g_BreakShowDesktop) },
|
||||
{ L"BreakOnSecondary", SETTING_TYPE_BOOLEAN, 0, &g_BreakOnSecondary,static_cast<DOUBLE>(g_BreakOnSecondary) },
|
||||
{ L"BreakLockWorkstation", SETTING_TYPE_BOOLEAN, 0, &g_BreakLockWorkstation, static_cast<DOUBLE>(g_BreakLockWorkstation) },
|
||||
{ L"FontScale", SETTING_TYPE_DWORD, 0, &g_FontScale, static_cast<DOUBLE>(g_FontScale) },
|
||||
{ L"ShowExpiredTime", SETTING_TYPE_BOOLEAN, 0, &g_ShowExpiredTime, static_cast<DOUBLE>(g_ShowExpiredTime) },
|
||||
{ L"ShowTrayIcon", SETTING_TYPE_BOOLEAN, 0, &g_ShowTrayIcon, static_cast<DOUBLE>(g_ShowTrayIcon) },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,4 +15,14 @@ RCZOOMIT64 BINRES MOVEABLE PURE RCZOOMIT_x64_path
|
||||
|
||||
#endif
|
||||
|
||||
// Embed the break timer screensaver for the current platform.
|
||||
// The .scr is built by the ZoomItBreak project into the shared output directory.
|
||||
#ifdef _M_IX86
|
||||
RCZOOMITSCR BINRES MOVEABLE PURE "ZoomItBreak.scr"
|
||||
#elif defined(_M_X64)
|
||||
RCZOOMITSCR BINRES MOVEABLE PURE "ZoomItBreak64.scr"
|
||||
#elif defined(_M_ARM64)
|
||||
RCZOOMITSCR BINRES MOVEABLE PURE "ZoomItBreak64a.scr"
|
||||
#endif
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "ZoomIt.exe.manifest"
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
#include <winrt/Windows.Storage.Pickers.h>
|
||||
#include <winrt/Windows.Storage.FileProperties.h>
|
||||
#include <winrt/Windows.Devices.Enumeration.h>
|
||||
#include <winrt/Windows.Media.Ocr.h>
|
||||
|
||||
#include <Windows.Graphics.Imaging.Interop.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
|
||||
@@ -78,6 +78,8 @@
|
||||
#define IDC_RECORD_FRAME_RATE2 1059
|
||||
#define IDC_RECORD_SCALING 1059
|
||||
#define IDC_SNIP_HOTKEY 1060
|
||||
#define IDC_SNIP_OCR_HOTKEY 1112
|
||||
#define IDC_SNIP_PANORAMA_HOTKEY 1114
|
||||
#define IDC_CAPTURE_AUDIO 1061
|
||||
#define IDC_MICROPHONE 1062
|
||||
#define IDC_PEN_CONTROL 1063
|
||||
@@ -111,12 +113,15 @@
|
||||
#define IDC_SMOOTH_IMAGE 1107
|
||||
#define IDC_CAPTURE_SYSTEM_AUDIO 1108
|
||||
#define IDC_MICROPHONE_LABEL 1109
|
||||
#define IDC_MIC_MONO_MIX 1110
|
||||
#define IDC_TRIM_FILE 1110
|
||||
#define IDC_MIC_MONO_MIX 1111
|
||||
#define IDC_CHECK_LOCK_WORKSTATION 1112
|
||||
#define IDC_SAVE 40002
|
||||
#define IDC_COPY 40004
|
||||
#define IDC_RECORD 40006
|
||||
#define IDC_RECORD_HOTKEY 40007
|
||||
#define IDC_COPY_CROP 40008
|
||||
#define IDC_COPY_OCR 40014
|
||||
#define IDC_SAVE_CROP 40009
|
||||
#define IDC_DEMOTYPE_HOTKEY 40011
|
||||
|
||||
@@ -125,8 +130,8 @@
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 120
|
||||
#define _APS_NEXT_COMMAND_VALUE 40013
|
||||
#define _APS_NEXT_CONTROL_VALUE 1099
|
||||
#define _APS_NEXT_COMMAND_VALUE 40015
|
||||
#define _APS_NEXT_CONTROL_VALUE 1113
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
|
||||
520
src/modules/ZoomIt/ZoomItBreak/BreakTimer.cpp
Normal file
520
src/modules/ZoomIt/ZoomItBreak/BreakTimer.cpp
Normal file
@@ -0,0 +1,520 @@
|
||||
//============================================================================
|
||||
//
|
||||
// BreakTimer.cpp
|
||||
//
|
||||
// Shared break timer rendering module used by both ZoomIt and the
|
||||
// ZoomItBreak screensaver (.scr).
|
||||
//
|
||||
// Copyright (C) Mark Russinovich
|
||||
// Sysinternals - www.sysinternals.com
|
||||
//
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
//============================================================================
|
||||
|
||||
// When built inside ZoomIt (with PCH), pch.h is included automatically.
|
||||
// When built for the screensaver project, we include the headers we need.
|
||||
#ifndef __ZOOMIT_SCREENSAVER__
|
||||
#include "pch.h"
|
||||
#endif
|
||||
|
||||
#include "BreakTimer.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
#pragma comment(lib, "Msimg32.lib")
|
||||
#pragma comment(lib, "Winmm.lib")
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakTimer_UpdateMonitorInfo
|
||||
//
|
||||
// Determine monitor geometry for the given screen point.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
void BreakTimer_UpdateMonitorInfo( POINT point, MONITORINFO* monInfo )
|
||||
{
|
||||
HMONITOR hMon = MonitorFromPoint( point, MONITOR_DEFAULTTONEAREST );
|
||||
if( hMon != nullptr )
|
||||
{
|
||||
monInfo->cbSize = sizeof *monInfo;
|
||||
GetMonitorInfo( hMon, monInfo );
|
||||
}
|
||||
else
|
||||
{
|
||||
*monInfo = {};
|
||||
HDC hdcScreen = CreateDC( L"DISPLAY", nullptr, nullptr, nullptr );
|
||||
if( hdcScreen != nullptr )
|
||||
{
|
||||
monInfo->rcMonitor.right = GetDeviceCaps( hdcScreen, HORZRES );
|
||||
monInfo->rcMonitor.bottom = GetDeviceCaps( hdcScreen, VERTRES );
|
||||
DeleteDC( hdcScreen );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakTimer_LoadImageFile
|
||||
//
|
||||
// Use GDI+ to load an image file and return an HBITMAP.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
HBITMAP BreakTimer_LoadImageFile( PTCHAR Filename )
|
||||
{
|
||||
HBITMAP hBmp;
|
||||
Gdiplus::Bitmap* bitmap = Gdiplus::Bitmap::FromFile( Filename );
|
||||
if( bitmap == nullptr || bitmap->GetHBITMAP( NULL, &hBmp ) != Gdiplus::Ok )
|
||||
{
|
||||
delete bitmap;
|
||||
return NULL;
|
||||
}
|
||||
delete bitmap;
|
||||
return hBmp;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakTimer_CreateFadedDesktopBackground
|
||||
//
|
||||
// Creates a snapshot of the desktop that is faded and alpha-blended
|
||||
// with black.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
HBITMAP BreakTimer_CreateFadedDesktopBackground( HDC hdc, LPRECT rcScreen, LPRECT rcCrop )
|
||||
{
|
||||
int width = rcScreen->right - rcScreen->left;
|
||||
int height = rcScreen->bottom - rcScreen->top;
|
||||
HDC hdcScreen = hdc;
|
||||
HDC hdcMem = CreateCompatibleDC( hdcScreen );
|
||||
HBITMAP hBitmap = CreateCompatibleBitmap( hdcScreen, width, height );
|
||||
HBITMAP hOld = static_cast<HBITMAP>( SelectObject( hdcMem, hBitmap ) );
|
||||
HBRUSH hBrush = CreateSolidBrush( RGB( 0, 0, 0 ) );
|
||||
|
||||
// Start with black background.
|
||||
FillRect( hdcMem, rcScreen, hBrush );
|
||||
if( rcCrop != NULL && rcCrop->left != -1 )
|
||||
{
|
||||
// Copy screen contents that are not cropped.
|
||||
BitBlt( hdcMem, rcCrop->left, rcCrop->top,
|
||||
rcCrop->right - rcCrop->left,
|
||||
rcCrop->bottom - rcCrop->top,
|
||||
hdcScreen, rcCrop->left, rcCrop->top, SRCCOPY );
|
||||
}
|
||||
|
||||
// Blend screen contents into the black background.
|
||||
BLENDFUNCTION blend = { 0 };
|
||||
blend.BlendOp = AC_SRC_OVER;
|
||||
blend.BlendFlags = 0;
|
||||
blend.SourceConstantAlpha = 0x4F;
|
||||
blend.AlphaFormat = 0;
|
||||
AlphaBlend( hdcMem, 0, 0, width, height,
|
||||
hdcScreen, rcScreen->left, rcScreen->top,
|
||||
width, height, blend );
|
||||
|
||||
SelectObject( hdcMem, hOld );
|
||||
DeleteDC( hdcMem );
|
||||
DeleteObject( hBrush );
|
||||
|
||||
return hBitmap;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakTimer_Init
|
||||
//
|
||||
// Create fonts, backing bitmap, and optionally load background.
|
||||
// Returns TRUE on success.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
BOOLEAN BreakTimer_Init(
|
||||
HWND hWnd,
|
||||
BreakTimerState* state,
|
||||
const BreakTimerSettings* settings,
|
||||
int timeoutSeconds,
|
||||
HBITMAP hExistingBackground,
|
||||
HDC hExistingBackgroundDC )
|
||||
{
|
||||
state->active = TRUE;
|
||||
state->timeoutSeconds = timeoutSeconds;
|
||||
|
||||
// Get screen DC.
|
||||
state->hdcScreen = CreateDC( L"DISPLAY", static_cast<PTCHAR>( NULL ),
|
||||
static_cast<PTCHAR>( NULL ),
|
||||
static_cast<CONST DEVMODE*>( NULL ) );
|
||||
if( !state->hdcScreen )
|
||||
return FALSE;
|
||||
|
||||
// Determine monitor.
|
||||
POINT cursorPos;
|
||||
GetCursorPos( &cursorPos );
|
||||
BreakTimer_UpdateMonitorInfo( cursorPos, &state->monInfo );
|
||||
state->width = state->monInfo.rcMonitor.right - state->monInfo.rcMonitor.left;
|
||||
state->height = state->monInfo.rcMonitor.bottom - state->monInfo.rcMonitor.top;
|
||||
|
||||
// Manage background bitmap.
|
||||
if( hExistingBackground )
|
||||
{
|
||||
// Caller supplied a pre-captured background (e.g. from command line).
|
||||
state->hBackgroundBmp = hExistingBackground;
|
||||
state->hDcBackgroundFile = hExistingBackgroundDC;
|
||||
}
|
||||
else if( settings->showBackgroundFile && !settings->showDesktop )
|
||||
{
|
||||
// Load image file.
|
||||
state->hBackgroundBmp = BreakTimer_LoadImageFile(
|
||||
const_cast<PTCHAR>( settings->backgroundFile ) );
|
||||
if( !state->hBackgroundBmp )
|
||||
return FALSE;
|
||||
state->hDcBackgroundFile = CreateCompatibleDC( state->hdcScreen );
|
||||
SelectObject( state->hDcBackgroundFile, state->hBackgroundBmp );
|
||||
}
|
||||
else if( settings->showBackgroundFile && settings->showDesktop )
|
||||
{
|
||||
// Faded desktop screenshot.
|
||||
HDC hDcDesktop = GetDC( NULL );
|
||||
state->hBackgroundBmp = BreakTimer_CreateFadedDesktopBackground(
|
||||
hDcDesktop, &state->monInfo.rcMonitor, NULL );
|
||||
ReleaseDC( NULL, hDcDesktop );
|
||||
state->hDcBackgroundFile = CreateCompatibleDC( state->hdcScreen );
|
||||
SelectObject( state->hDcBackgroundFile, state->hBackgroundBmp );
|
||||
}
|
||||
else
|
||||
{
|
||||
state->hBackgroundBmp = NULL;
|
||||
state->hDcBackgroundFile = NULL;
|
||||
}
|
||||
|
||||
// Create fonts.
|
||||
LOGFONT lf = settings->logFont;
|
||||
lf.lfHeight = state->height / 5;
|
||||
state->hTimerFont = CreateFontIndirect( &lf );
|
||||
lf.lfHeight = state->height / 8;
|
||||
state->hNegativeTimerFont = CreateFontIndirect( &lf );
|
||||
|
||||
// Create backing bitmap for double buffering.
|
||||
state->hdcScreenCompat = CreateCompatibleDC( state->hdcScreen );
|
||||
state->bmp.bmBitsPixel = static_cast<BYTE>( GetDeviceCaps( state->hdcScreen, BITSPIXEL ) );
|
||||
state->bmp.bmPlanes = static_cast<BYTE>( GetDeviceCaps( state->hdcScreen, PLANES ) );
|
||||
state->bmp.bmWidth = state->width;
|
||||
state->bmp.bmHeight = state->height;
|
||||
state->bmp.bmWidthBytes = ( ( state->bmp.bmWidth + 15 ) & ~15 ) / 8;
|
||||
state->hbmpCompat = CreateBitmap( state->bmp.bmWidth, state->bmp.bmHeight,
|
||||
state->bmp.bmPlanes, state->bmp.bmBitsPixel, static_cast<CONST VOID*>( NULL ) );
|
||||
SelectObject( state->hdcScreenCompat, state->hbmpCompat );
|
||||
|
||||
SetTextColor( state->hdcScreenCompat, settings->penColor );
|
||||
SetBkMode( state->hdcScreenCompat, TRANSPARENT );
|
||||
SelectObject( state->hdcScreenCompat, state->hTimerFont );
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakTimer_Tick
|
||||
//
|
||||
// Decrement counter, invalidate window, play sound at zero.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
void BreakTimer_Tick(
|
||||
HWND hWnd,
|
||||
BreakTimerState* state,
|
||||
const BreakTimerSettings* settings )
|
||||
{
|
||||
state->timeoutSeconds -= 1;
|
||||
InvalidateRect( hWnd, NULL, FALSE );
|
||||
|
||||
if( state->timeoutSeconds == 0 && settings->playSound )
|
||||
{
|
||||
PlaySound( settings->soundFile, NULL, SND_FILENAME | SND_ASYNC );
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakTimer_Paint
|
||||
//
|
||||
// Render the break timer into the back buffer and blit to the paint DC.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
void BreakTimer_Paint(
|
||||
HDC hdc,
|
||||
BreakTimerState* state,
|
||||
const BreakTimerSettings* settings )
|
||||
{
|
||||
RECT rc, rc1;
|
||||
TCHAR timerText[16];
|
||||
TCHAR negativeTimerText[16];
|
||||
|
||||
// Fill background (white by default, black if backgroundColor == 1).
|
||||
rc.top = rc.left = 0;
|
||||
rc.bottom = state->height;
|
||||
rc.right = state->width;
|
||||
if( settings->backgroundColor )
|
||||
{
|
||||
HBRUSH hBrush = CreateSolidBrush( RGB( 0, 0, 0 ) );
|
||||
FillRect( state->hdcScreenCompat, &rc, hBrush );
|
||||
DeleteObject( hBrush );
|
||||
}
|
||||
else
|
||||
{
|
||||
FillRect( state->hdcScreenCompat, &rc, GetSysColorBrush( COLOR_WINDOW ) );
|
||||
}
|
||||
|
||||
// Draw background bitmap if present.
|
||||
if( state->hBackgroundBmp )
|
||||
{
|
||||
BITMAP local_bmp;
|
||||
GetObject( state->hBackgroundBmp, sizeof( local_bmp ), &local_bmp );
|
||||
SetStretchBltMode( state->hdcScreenCompat,
|
||||
settings->smoothImage ? HALFTONE : COLORONCOLOR );
|
||||
if( settings->backgroundStretch )
|
||||
{
|
||||
StretchBlt( state->hdcScreenCompat, 0, 0, state->width, state->height,
|
||||
state->hDcBackgroundFile, 0, 0,
|
||||
local_bmp.bmWidth, local_bmp.bmHeight, SRCCOPY | CAPTUREBLT );
|
||||
}
|
||||
else
|
||||
{
|
||||
BitBlt( state->hdcScreenCompat,
|
||||
state->width / 2 - local_bmp.bmWidth / 2,
|
||||
state->height / 2 - local_bmp.bmHeight / 2,
|
||||
local_bmp.bmWidth, local_bmp.bmHeight,
|
||||
state->hDcBackgroundFile, 0, 0, SRCCOPY | CAPTUREBLT );
|
||||
}
|
||||
}
|
||||
|
||||
// Format timer text.
|
||||
if( state->timeoutSeconds > 0 )
|
||||
{
|
||||
_stprintf( timerText, L"% 2d:%02d",
|
||||
state->timeoutSeconds / 60, state->timeoutSeconds % 60 );
|
||||
}
|
||||
else
|
||||
{
|
||||
_tcscpy( timerText, L"0:00" );
|
||||
}
|
||||
|
||||
// Measure timer text.
|
||||
rc.left = rc.top = 0;
|
||||
DrawText( state->hdcScreenCompat, timerText, -1, &rc,
|
||||
DT_NOCLIP | DT_LEFT | DT_NOPREFIX | DT_CALCRECT );
|
||||
|
||||
// Measure expired text if needed.
|
||||
rc1.left = rc1.right = rc1.bottom = rc1.top = 0;
|
||||
if( settings->showExpiredTime && state->timeoutSeconds < 0 )
|
||||
{
|
||||
_stprintf( negativeTimerText, L"(-% 2d:%02d)",
|
||||
-state->timeoutSeconds / 60, -state->timeoutSeconds % 60 );
|
||||
HFONT prevFont = static_cast<HFONT>(
|
||||
SelectObject( state->hdcScreenCompat, state->hNegativeTimerFont ) );
|
||||
DrawText( state->hdcScreenCompat, negativeTimerText, -1, &rc1,
|
||||
DT_NOCLIP | DT_LEFT | DT_NOPREFIX | DT_CALCRECT );
|
||||
SelectObject( state->hdcScreenCompat, prevFont );
|
||||
}
|
||||
|
||||
// Position vertically.
|
||||
switch( settings->timerPosition )
|
||||
{
|
||||
case 0: case 1: case 2:
|
||||
rc.top = 50;
|
||||
break;
|
||||
case 3: case 4: case 5:
|
||||
rc.top = ( state->height - ( rc.bottom - rc.top ) ) / 2;
|
||||
break;
|
||||
case 6: case 7: case 8:
|
||||
rc.top = state->height - rc.bottom - 50 - rc1.bottom;
|
||||
break;
|
||||
}
|
||||
|
||||
// Position horizontally.
|
||||
switch( settings->timerPosition )
|
||||
{
|
||||
case 0: case 3: case 6:
|
||||
rc.left = 50;
|
||||
break;
|
||||
case 1: case 4: case 7:
|
||||
rc.left = ( state->width - ( rc.right - rc.left ) ) / 2;
|
||||
break;
|
||||
case 2: case 5: case 8:
|
||||
rc.left = state->width - rc.right - 50;
|
||||
break;
|
||||
}
|
||||
rc.bottom += rc.top;
|
||||
rc.right += rc.left;
|
||||
|
||||
// Draw timer text.
|
||||
DrawText( state->hdcScreenCompat, timerText, -1, &rc,
|
||||
DT_NOCLIP | DT_LEFT | DT_NOPREFIX );
|
||||
|
||||
// Draw expired text below the timer.
|
||||
if( settings->showExpiredTime && state->timeoutSeconds < 0 )
|
||||
{
|
||||
rc1.top = rc.bottom + 10;
|
||||
rc1.left = rc.left + ( ( rc.right - rc.left ) - ( rc1.right - rc1.left ) ) / 2;
|
||||
HFONT prevFont = static_cast<HFONT>(
|
||||
SelectObject( state->hdcScreenCompat, state->hNegativeTimerFont ) );
|
||||
DrawText( state->hdcScreenCompat, negativeTimerText, -1, &rc1,
|
||||
DT_NOCLIP | DT_LEFT | DT_NOPREFIX );
|
||||
SelectObject( state->hdcScreenCompat, prevFont );
|
||||
}
|
||||
|
||||
// Copy to screen.
|
||||
BitBlt( hdc, 0, 0, state->width, state->height,
|
||||
state->hdcScreenCompat, 0, 0, SRCCOPY | CAPTUREBLT );
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakTimer_Cleanup
|
||||
//
|
||||
// Free the GDI resources used by the break timer.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
void BreakTimer_Cleanup(
|
||||
BreakTimerState* state,
|
||||
BOOLEAN freeBackground )
|
||||
{
|
||||
if( freeBackground && state->hBackgroundBmp )
|
||||
{
|
||||
DeleteObject( state->hBackgroundBmp );
|
||||
DeleteDC( state->hDcBackgroundFile );
|
||||
state->hBackgroundBmp = NULL;
|
||||
state->hDcBackgroundFile = NULL;
|
||||
}
|
||||
|
||||
if( state->hTimerFont )
|
||||
{
|
||||
DeleteObject( state->hTimerFont );
|
||||
state->hTimerFont = NULL;
|
||||
}
|
||||
if( state->hNegativeTimerFont )
|
||||
{
|
||||
DeleteObject( state->hNegativeTimerFont );
|
||||
state->hNegativeTimerFont = NULL;
|
||||
}
|
||||
if( state->hdcScreen )
|
||||
{
|
||||
DeleteDC( state->hdcScreen );
|
||||
state->hdcScreen = NULL;
|
||||
}
|
||||
if( state->hdcScreenCompat )
|
||||
{
|
||||
DeleteDC( state->hdcScreenCompat );
|
||||
state->hdcScreenCompat = NULL;
|
||||
}
|
||||
if( state->hbmpCompat )
|
||||
{
|
||||
DeleteObject( state->hbmpCompat );
|
||||
state->hbmpCompat = NULL;
|
||||
}
|
||||
|
||||
state->active = FALSE;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakTimer_AdjustTime
|
||||
//
|
||||
// Round to the nearest minute boundary and adjust by deltaMinutes.
|
||||
// Resets the 1-second timer on the window.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
void BreakTimer_AdjustTime(
|
||||
HWND hWnd,
|
||||
BreakTimerState* state,
|
||||
int deltaMinutes )
|
||||
{
|
||||
int breakTimeout = state->timeoutSeconds;
|
||||
|
||||
if( deltaMinutes > 0 )
|
||||
{
|
||||
if( breakTimeout < 0 ) breakTimeout = 0;
|
||||
if( breakTimeout % 60 )
|
||||
{
|
||||
breakTimeout += ( 60 - breakTimeout % 60 );
|
||||
deltaMinutes--;
|
||||
}
|
||||
breakTimeout += deltaMinutes * 60;
|
||||
}
|
||||
else
|
||||
{
|
||||
int absDelta = -deltaMinutes;
|
||||
if( breakTimeout % 60 )
|
||||
{
|
||||
breakTimeout -= breakTimeout % 60;
|
||||
absDelta--;
|
||||
}
|
||||
breakTimeout -= absDelta * 60;
|
||||
}
|
||||
|
||||
if( breakTimeout < 0 ) breakTimeout = 0;
|
||||
state->timeoutSeconds = breakTimeout;
|
||||
|
||||
KillTimer( hWnd, 0 );
|
||||
SetTimer( hWnd, 0, 1000, NULL );
|
||||
InvalidateRect( hWnd, NULL, TRUE );
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakScrConfig_GetPath
|
||||
//
|
||||
// Build the full path to the config file in %TEMP%.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
static void BreakScrConfig_GetPath( TCHAR* path, size_t cch )
|
||||
{
|
||||
GetTempPath( static_cast<DWORD>( cch ), path );
|
||||
_tcscat( path, BREAKSCR_CONFIG_FILE );
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakScrConfig_Write
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
BOOLEAN BreakScrConfig_Write( const BreakScrConfig* config )
|
||||
{
|
||||
TCHAR path[MAX_PATH];
|
||||
BreakScrConfig_GetPath( path, MAX_PATH );
|
||||
|
||||
HANDLE hFile = CreateFile( path, GENERIC_WRITE, 0, NULL,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
|
||||
if( hFile == INVALID_HANDLE_VALUE )
|
||||
return FALSE;
|
||||
|
||||
DWORD written;
|
||||
BOOL ok = WriteFile( hFile, config, sizeof( *config ), &written, NULL );
|
||||
CloseHandle( hFile );
|
||||
return ok && written == sizeof( *config );
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// BreakScrConfig_Read
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
BOOLEAN BreakScrConfig_Read( BreakScrConfig* config )
|
||||
{
|
||||
TCHAR path[MAX_PATH];
|
||||
BreakScrConfig_GetPath( path, MAX_PATH );
|
||||
|
||||
HANDLE hFile = CreateFile( path, GENERIC_READ, FILE_SHARE_READ, NULL,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
|
||||
if( hFile == INVALID_HANDLE_VALUE )
|
||||
return FALSE;
|
||||
|
||||
DWORD bytesRead;
|
||||
BOOL ok = ReadFile( hFile, config, sizeof( *config ), &bytesRead, NULL );
|
||||
CloseHandle( hFile );
|
||||
|
||||
if( !ok || bytesRead != sizeof( *config ) )
|
||||
return FALSE;
|
||||
if( config->magic != BREAKSCR_CONFIG_MAGIC )
|
||||
return FALSE;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
141
src/modules/ZoomIt/ZoomItBreak/BreakTimer.h
Normal file
141
src/modules/ZoomIt/ZoomItBreak/BreakTimer.h
Normal file
@@ -0,0 +1,141 @@
|
||||
//============================================================================
|
||||
//
|
||||
// BreakTimer.h
|
||||
//
|
||||
// Shared break timer rendering module used by both ZoomIt and the
|
||||
// ZoomItBreak screensaver (.scr).
|
||||
//
|
||||
// Copyright (C) Mark Russinovich
|
||||
// Sysinternals - www.sysinternals.com
|
||||
//
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
//============================================================================
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <tchar.h>
|
||||
#define GDIPVER 0x0110
|
||||
#include <gdiplus.h>
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// BreakTimerSettings — read-only configuration, populated from globals
|
||||
// or from command-line arguments in the screensaver.
|
||||
//----------------------------------------------------------------------------
|
||||
struct BreakTimerSettings
|
||||
{
|
||||
DWORD penColor;
|
||||
DWORD backgroundColor; // 0 = white, 1 = black
|
||||
DWORD timerPosition; // 0–8 (3×3 grid)
|
||||
DWORD opacity; // 0–100
|
||||
DWORD showExpiredTime; // 0 or 1
|
||||
BOOLEAN smoothImage;
|
||||
BOOLEAN backgroundStretch;
|
||||
BOOLEAN playSound;
|
||||
TCHAR soundFile[MAX_PATH];
|
||||
BOOLEAN showDesktop;
|
||||
BOOLEAN showBackgroundFile;
|
||||
TCHAR backgroundFile[MAX_PATH];
|
||||
LOGFONT logFont;
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// BreakTimerState — runtime state for an active break timer.
|
||||
//----------------------------------------------------------------------------
|
||||
struct BreakTimerState
|
||||
{
|
||||
BOOLEAN active;
|
||||
int timeoutSeconds; // counts down; goes negative if expired
|
||||
HFONT hTimerFont;
|
||||
HFONT hNegativeTimerFont;
|
||||
HBITMAP hBackgroundBmp;
|
||||
HDC hDcBackgroundFile;
|
||||
HDC hdcScreen;
|
||||
HDC hdcScreenCompat;
|
||||
HBITMAP hbmpCompat;
|
||||
BITMAP bmp;
|
||||
int width;
|
||||
int height;
|
||||
MONITORINFO monInfo;
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Shared utility functions
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
// Determine monitor geometry for the given screen point.
|
||||
void BreakTimer_UpdateMonitorInfo( POINT point, MONITORINFO* monInfo );
|
||||
|
||||
// Load an image file via GDI+; returns an HBITMAP or NULL on failure.
|
||||
HBITMAP BreakTimer_LoadImageFile( PTCHAR Filename );
|
||||
|
||||
// Capture a faded (alpha-blended with black) screenshot of the desktop.
|
||||
HBITMAP BreakTimer_CreateFadedDesktopBackground( HDC hdc, LPRECT rcScreen, LPRECT rcCrop );
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Break timer lifecycle
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
// Create fonts, backing bitmap, and load background.
|
||||
// The caller is responsible for creating/showing the window itself.
|
||||
// |timeoutSeconds| is already in seconds (e.g. g_BreakTimeout * 60 + 1).
|
||||
BOOLEAN BreakTimer_Init(
|
||||
HWND hWnd,
|
||||
BreakTimerState* state,
|
||||
const BreakTimerSettings* settings,
|
||||
int timeoutSeconds,
|
||||
HBITMAP hExistingBackground, // optional pre-captured background
|
||||
HDC hExistingBackgroundDC // optional DC for above
|
||||
);
|
||||
|
||||
// Called every second; decrements the counter and invalidates the window.
|
||||
void BreakTimer_Tick(
|
||||
HWND hWnd,
|
||||
BreakTimerState* state,
|
||||
const BreakTimerSettings* settings
|
||||
);
|
||||
|
||||
// Render the timer into hdcScreenCompat then BitBlt to hdc (from BeginPaint).
|
||||
void BreakTimer_Paint(
|
||||
HDC hdc,
|
||||
BreakTimerState* state,
|
||||
const BreakTimerSettings* settings
|
||||
);
|
||||
|
||||
// Free fonts, DCs, bitmaps. If |freeBackground| is false the background
|
||||
// bitmap/DC are left for the caller to manage (e.g. shallow destroy).
|
||||
void BreakTimer_Cleanup(
|
||||
BreakTimerState* state,
|
||||
BOOLEAN freeBackground
|
||||
);
|
||||
|
||||
// Adjust the remaining time by |deltaMinutes| (positive = add time).
|
||||
// Resets the 1-second timer on hWnd.
|
||||
void BreakTimer_AdjustTime(
|
||||
HWND hWnd,
|
||||
BreakTimerState* state,
|
||||
int deltaMinutes
|
||||
);
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// BreakScrConfig — binary blob written to a temp file by ZoomIt and
|
||||
// read by the screensaver on startup. This avoids command-line arg
|
||||
// issues since Windows launches screensavers with only /s.
|
||||
//----------------------------------------------------------------------------
|
||||
#define BREAKSCR_CONFIG_MAGIC 0x5A4D4253 // 'ZMBS'
|
||||
#define BREAKSCR_CONFIG_FILE L"ZoomItBreakConfig.dat"
|
||||
|
||||
struct BreakScrConfig
|
||||
{
|
||||
DWORD magic; // must be BREAKSCR_CONFIG_MAGIC
|
||||
int timeoutSeconds;
|
||||
BOOL resumed; // set TRUE by screensaver on first launch
|
||||
BreakTimerSettings settings;
|
||||
TCHAR screenshotPath[MAX_PATH];
|
||||
};
|
||||
|
||||
// Write config to %TEMP%\BREAKSCR_CONFIG_FILE.
|
||||
BOOLEAN BreakScrConfig_Write( const BreakScrConfig* config );
|
||||
|
||||
// Read config from %TEMP%\BREAKSCR_CONFIG_FILE.
|
||||
BOOLEAN BreakScrConfig_Read( BreakScrConfig* config );
|
||||
10
src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.manifest
Normal file
10
src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.manifest
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"
|
||||
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
27
src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.rc
Normal file
27
src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.rc
Normal file
@@ -0,0 +1,27 @@
|
||||
//============================================================================
|
||||
//
|
||||
// ZoomItBreak.rc
|
||||
//
|
||||
// Minimal resources required by Scrnsavw.lib.
|
||||
//
|
||||
//============================================================================
|
||||
#include <windows.h>
|
||||
#include <scrnsave.h>
|
||||
|
||||
// Embed DPI-awareness manifest so the screensaver sees native resolution.
|
||||
// This ensures the pre-captured desktop screenshot (saved at physical pixels
|
||||
// by the DPI-aware ZoomIt process) matches the screensaver window dimensions.
|
||||
1 RT_MANIFEST "ZoomItBreak.manifest"
|
||||
|
||||
// IDS_DESCRIPTION is used by scrnsavw.lib as the window class name.
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
IDS_DESCRIPTION, "ZoomIt Break Timer"
|
||||
END
|
||||
|
||||
// Stub configuration dialog - never shown (ScreenSaverConfigureDialog returns FALSE).
|
||||
DLG_SCRNSAVECONFIGURE DIALOG 0, 0, 200, 60
|
||||
STYLE WS_DLGFRAME | WS_POPUP | WS_VISIBLE | DS_MODALFRAME | WS_CAPTION
|
||||
CAPTION "ZoomIt Break Timer"
|
||||
BEGIN
|
||||
END
|
||||
230
src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.vcxproj
Normal file
230
src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.vcxproj
Normal file
@@ -0,0 +1,230 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>18.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{94ba3051-c8d7-454a-9d46-1a7c78e228a3}</ProjectGuid>
|
||||
<RootNamespace>ZoomItBreak</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<!--
|
||||
Output .scr instead of .exe. A screensaver is a renamed executable.
|
||||
Use a separate intermediate dir to avoid colliding with ZoomIt.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<TargetExt>.scr</TargetExt>
|
||||
<GenerateManifest>false</GenerateManifest>
|
||||
<IntDir>$(Platform)\$(Configuration)\ZoomItBreak\</IntDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
|
||||
<TargetName>$(ProjectName)64</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
|
||||
<TargetName>$(ProjectName)64a</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
|
||||
<TargetName>$(ProjectName)64</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<OutDir>$(MsBuildProjectDirectory)\$(Platform)\$(Configuration)\</OutDir>
|
||||
<TargetName>$(ProjectName)64a</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<DisableSpecificWarnings>4100;4091;4245</DisableSpecificWarnings>
|
||||
<AdditionalIncludeDirectories>..\..\..\;$(MSBuildThisFileDirectory)..\..\..\common\sysinternals;..\ZoomIt;%(AdditionalIncludeDirectories);</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<LanguageStandard_C>stdc17</LanguageStandard_C>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>__ZOOMIT_SCREENSAVER__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="BreakTimer.cpp" />
|
||||
<ClCompile Include="ZoomItBreakScr.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="ZoomItBreak.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="BreakTimer.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
</Project>
|
||||
38
src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.vcxproj.filters
Normal file
38
src/modules/ZoomIt/ZoomItBreak/ZoomItBreak.vcxproj.filters
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="BreakTimer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ZoomItBreakScr.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="BreakTimer.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="ZoomItBreak.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
283
src/modules/ZoomIt/ZoomItBreak/ZoomItBreakScr.cpp
Normal file
283
src/modules/ZoomIt/ZoomItBreak/ZoomItBreakScr.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
//============================================================================
|
||||
//
|
||||
// ZoomItBreakScr.cpp
|
||||
//
|
||||
// ZoomIt break timer screensaver (.scr). When launched by Winlogon on the
|
||||
// Screen-saver desktop with password protection, the user must authenticate
|
||||
// to dismiss it. The break timer countdown and rendering are provided by
|
||||
// the shared BreakTimer module.
|
||||
//
|
||||
// Copyright (C) Mark Russinovich
|
||||
// Sysinternals - www.sysinternals.com
|
||||
//
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
//============================================================================
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
#include <tchar.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <scrnsave.h>
|
||||
#define GDIPVER 0x0110
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include "BreakTimer.h"
|
||||
|
||||
static void DbgPrint( LPCTSTR fmt, ... )
|
||||
{
|
||||
TCHAR buf[512];
|
||||
va_list ap;
|
||||
#pragma warning( push )
|
||||
#pragma warning( disable : 26492 )
|
||||
va_start( ap, fmt );
|
||||
#pragma warning( pop )
|
||||
_vsntprintf( buf, _countof(buf), fmt, ap );
|
||||
va_end( ap );
|
||||
buf[_countof(buf)-1] = 0;
|
||||
OutputDebugString( buf );
|
||||
}
|
||||
|
||||
#pragma comment(lib, "scrnsavw.lib")
|
||||
#pragma comment(lib, "comctl32.lib")
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
#pragma comment(lib, "Msimg32.lib")
|
||||
#pragma comment(lib, "Winmm.lib")
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Globals
|
||||
//----------------------------------------------------------------------------
|
||||
static BreakTimerSettings g_Settings;
|
||||
static BreakTimerState g_State;
|
||||
static ULONG_PTR g_GdiplusToken;
|
||||
static TCHAR g_ScreenshotPath[MAX_PATH] = { 0 };
|
||||
static int g_LastSavedTimeout = 0; // For state persistence
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Load settings from the binary config file written by ZoomIt,
|
||||
// falling back to hard-coded defaults if the file is missing.
|
||||
//----------------------------------------------------------------------------
|
||||
static void LoadSettings( void )
|
||||
{
|
||||
BreakScrConfig config;
|
||||
if( BreakScrConfig_Read( &config ) )
|
||||
{
|
||||
g_Settings = config.settings;
|
||||
g_State.timeoutSeconds = config.timeoutSeconds;
|
||||
_tcscpy( g_ScreenshotPath, config.screenshotPath );
|
||||
DbgPrint( L"[BreakScr] Config loaded: timeout=%d, bgFile=%d, showDesktop=%d, screenshot=%s\n",
|
||||
config.timeoutSeconds, config.settings.showBackgroundFile,
|
||||
config.settings.showDesktop, config.screenshotPath );
|
||||
return;
|
||||
}
|
||||
|
||||
DbgPrint( L"[BreakScr] Config file not found, using fallback defaults\n" );
|
||||
// Fallback defaults (for testing the .scr directly).
|
||||
memset( &g_Settings, 0, sizeof( g_Settings ) );
|
||||
g_Settings.penColor = RGB( 255, 0, 0 );
|
||||
g_Settings.timerPosition = 4;
|
||||
g_Settings.opacity = 100;
|
||||
g_Settings.showExpiredTime = 1;
|
||||
g_Settings.smoothImage = TRUE;
|
||||
g_Settings.backgroundStretch = FALSE;
|
||||
g_Settings.showDesktop = TRUE;
|
||||
g_Settings.showBackgroundFile = FALSE;
|
||||
g_State.timeoutSeconds = 600;
|
||||
|
||||
NONCLIENTMETRICS ncm = { sizeof( ncm ) };
|
||||
SystemParametersInfo( SPI_GETNONCLIENTMETRICS, sizeof( ncm ), &ncm, 0 );
|
||||
g_Settings.logFont = ncm.lfMessageFont;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// ScreenSaverProc
|
||||
//
|
||||
// Main window procedure for the screensaver, called by Scrnsavw.lib.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
LRESULT WINAPI ScreenSaverProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
|
||||
{
|
||||
switch( msg )
|
||||
{
|
||||
case WM_CREATE:
|
||||
{
|
||||
DbgPrint( L"[BreakScr] WM_CREATE: hwnd=%p\n", hWnd );
|
||||
|
||||
// Initialize GDI+.
|
||||
Gdiplus::GdiplusStartupInput startupIn;
|
||||
Gdiplus::GdiplusStartup( &g_GdiplusToken, &startupIn, NULL );
|
||||
|
||||
LoadSettings();
|
||||
|
||||
// Check if a previous screensaver instance already ran (resumed == TRUE).
|
||||
// On first launch, ZoomIt sets resumed = FALSE, so we skip the deduction.
|
||||
BreakScrConfig resumeConfig;
|
||||
if( BreakScrConfig_Read( &resumeConfig ) && resumeConfig.resumed )
|
||||
{
|
||||
// Subtract the screensaver idle timeout to compensate for
|
||||
// the time the screensaver wasn't running on the lock screen.
|
||||
UINT scrTimeout = 0;
|
||||
SystemParametersInfo( SPI_GETSCREENSAVETIMEOUT, 0, &scrTimeout, 0 );
|
||||
g_State.timeoutSeconds -= static_cast<int>( scrTimeout );
|
||||
if( g_State.timeoutSeconds < 0 && !g_Settings.showExpiredTime )
|
||||
g_State.timeoutSeconds = 0;
|
||||
DbgPrint( L"[BreakScr] Resumption: subtracted %u sec idle, timeout=%d\n",
|
||||
scrTimeout, g_State.timeoutSeconds );
|
||||
}
|
||||
|
||||
// Mark as resumed so subsequent screensaver launches know to deduct idle time.
|
||||
{
|
||||
BreakScrConfig markConfig;
|
||||
if( BreakScrConfig_Read( &markConfig ) )
|
||||
{
|
||||
markConfig.resumed = TRUE;
|
||||
BreakScrConfig_Write( &markConfig );
|
||||
}
|
||||
}
|
||||
|
||||
// Load pre-captured screenshot if provided.
|
||||
HBITMAP hBgBmp = NULL;
|
||||
HDC hBgDC = NULL;
|
||||
if( g_ScreenshotPath[0] )
|
||||
{
|
||||
hBgBmp = BreakTimer_LoadImageFile( g_ScreenshotPath );
|
||||
DbgPrint( L"[BreakScr] LoadImageFile(%s) => %p\n", g_ScreenshotPath, hBgBmp );
|
||||
if( hBgBmp )
|
||||
{
|
||||
HDC hdcScreen = CreateDC( L"DISPLAY", NULL, NULL, NULL );
|
||||
hBgDC = CreateCompatibleDC( hdcScreen );
|
||||
SelectObject( hBgDC, hBgBmp );
|
||||
DeleteDC( hdcScreen );
|
||||
}
|
||||
}
|
||||
|
||||
int timeout = g_State.timeoutSeconds;
|
||||
memset( &g_State, 0, sizeof( g_State ) );
|
||||
|
||||
DbgPrint( L"[BreakScr] Calling BreakTimer_Init, timeout=%d\n", timeout );
|
||||
if( !BreakTimer_Init( hWnd, &g_State, &g_Settings, timeout, hBgBmp, hBgDC ) )
|
||||
{
|
||||
DbgPrint( L"[BreakScr] BreakTimer_Init FAILED\n" );
|
||||
PostMessage( hWnd, WM_CLOSE, 0, 0 );
|
||||
return 0;
|
||||
}
|
||||
DbgPrint( L"[BreakScr] BreakTimer_Init OK, active=%d\n", g_State.active );
|
||||
|
||||
// Prevent the monitor from blanking due to power management.
|
||||
SetThreadExecutionState( ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED );
|
||||
|
||||
// Kick off the first tick and start the 1-second timer.
|
||||
SendMessage( hWnd, WM_TIMER, 1, 0 );
|
||||
SetTimer( hWnd, 1, 1000, NULL );
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_TIMER:
|
||||
if( wParam == 1 )
|
||||
{
|
||||
BreakTimer_Tick( hWnd, &g_State, &g_Settings );
|
||||
|
||||
// Periodically save state (every 5 seconds) for resumption after
|
||||
// credential provider timeout. This allows the screensaver to continue
|
||||
// from where it left off if a student triggers the login screen but
|
||||
// doesn't authenticate.
|
||||
if( g_State.timeoutSeconds != g_LastSavedTimeout &&
|
||||
g_State.timeoutSeconds % 5 == 0 )
|
||||
{
|
||||
BreakScrConfig config;
|
||||
if( BreakScrConfig_Read( &config ) )
|
||||
{
|
||||
config.timeoutSeconds = g_State.timeoutSeconds;
|
||||
if( BreakScrConfig_Write( &config ) )
|
||||
{
|
||||
g_LastSavedTimeout = g_State.timeoutSeconds;
|
||||
DbgPrint( L"[BreakScr] Saved state: %d seconds remaining\n",
|
||||
g_State.timeoutSeconds );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint( hWnd, &ps );
|
||||
if( g_State.active )
|
||||
{
|
||||
BreakTimer_Paint( hdc, &g_State, &g_Settings );
|
||||
}
|
||||
EndPaint( hWnd, &ps );
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_DESTROY:
|
||||
DbgPrint( L"[BreakScr] WM_DESTROY\n" );
|
||||
SetThreadExecutionState( ES_CONTINUOUS ); // Restore default power behavior
|
||||
KillTimer( hWnd, 1 );
|
||||
BreakTimer_Cleanup( &g_State, TRUE );
|
||||
Gdiplus::GdiplusShutdown( g_GdiplusToken );
|
||||
return 0;
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// Prevent DefScreenSaverProc from auto-closing on user input.
|
||||
// The screensaver must stay up until the break timer expires or
|
||||
// the user authenticates via Ctrl+Alt+Del. DefScreenSaverProc
|
||||
// would close the window on mouse movement, clicks, keyboard,
|
||||
// or deactivation.
|
||||
//------------------------------------------------------------------
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_LBUTTONDOWN:
|
||||
case WM_RBUTTONDOWN:
|
||||
case WM_MBUTTONDOWN:
|
||||
case WM_KEYDOWN:
|
||||
if( wParam == 'W' || wParam == 'K' )
|
||||
{
|
||||
g_Settings.backgroundColor = ( wParam == 'K' ) ? 1 : 0;
|
||||
InvalidateRect( hWnd, NULL, FALSE );
|
||||
}
|
||||
return 0;
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYDOWN:
|
||||
return 0;
|
||||
|
||||
case WM_ACTIVATE:
|
||||
case WM_ACTIVATEAPP:
|
||||
// Don't close on deactivation (e.g. LockWorkStation switches desktop).
|
||||
return 0;
|
||||
|
||||
case WM_SYSCOMMAND:
|
||||
// Block SC_CLOSE from Alt+F4 etc.
|
||||
if( ( wParam & 0xFFF0 ) == SC_CLOSE )
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return DefScreenSaverProc( hWnd, msg, wParam, lParam );
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// ScreenSaverConfigureDialog
|
||||
//
|
||||
// No configuration — ZoomIt handles all settings.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
BOOL WINAPI ScreenSaverConfigureDialog( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// RegisterDialogClasses
|
||||
//
|
||||
// Nothing to register.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
BOOL WINAPI RegisterDialogClasses( HANDLE hInst )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
@@ -91,3 +91,12 @@ void Trace::ZoomItActivateSnip() noexcept
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
|
||||
void Trace::ZoomItActivateSnipOcr() noexcept
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"ZoomIt_ActivateSnipOcr",
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
|
||||
@@ -14,4 +14,5 @@ public:
|
||||
static void ZoomItActivateDemoType() noexcept;
|
||||
static void ZoomItActivateRecord() noexcept;
|
||||
static void ZoomItActivateSnip() noexcept;
|
||||
static void ZoomItActivateSnipOcr() noexcept;
|
||||
};
|
||||
|
||||
@@ -70,6 +70,8 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
|
||||
{ L"DrawToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"RecordToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipOcrToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"SnipPanoramaToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"BreakTimerKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"DemoTypeToggleKey", SPECIAL_SEMANTICS_SHORTCUT },
|
||||
{ L"PenColor", SPECIAL_SEMANTICS_COLOR },
|
||||
|
||||
@@ -76,7 +76,7 @@ bool isExcluded(HWND window)
|
||||
}
|
||||
|
||||
AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
|
||||
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps, SettingId::ShowInSystemMenu}),
|
||||
SettingsObserver({ SettingId::FrameEnabled, SettingId::Hotkey, SettingId::IncreaseOpacityHotkey, SettingId::DecreaseOpacityHotkey, SettingId::ExcludeApps, SettingId::ShowInSystemMenu }),
|
||||
m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase)),
|
||||
m_useCentralizedLLKH(useLLKH),
|
||||
m_mainThreadId(mainThreadId),
|
||||
@@ -150,6 +150,8 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
|
||||
switch (id)
|
||||
{
|
||||
case SettingId::Hotkey:
|
||||
case SettingId::IncreaseOpacityHotkey:
|
||||
case SettingId::DecreaseOpacityHotkey:
|
||||
{
|
||||
RegisterHotkey();
|
||||
}
|
||||
@@ -360,10 +362,8 @@ void AlwaysOnTop::RegisterHotkey() const
|
||||
// Register pin hotkey
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), settings->hotkey.get_modifiers(), settings->hotkey.get_code());
|
||||
|
||||
// Register transparency hotkeys using the same modifiers as the pin hotkey
|
||||
UINT modifiers = settings->hotkey.get_modifiers();
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), settings->increaseOpacityHotkey.get_modifiers(), settings->increaseOpacityHotkey.get_code());
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), settings->decreaseOpacityHotkey.get_modifiers(), settings->decreaseOpacityHotkey.get_code());
|
||||
}
|
||||
|
||||
void AlwaysOnTop::RegisterLLKH()
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace NonLocalizable
|
||||
const static wchar_t* SettingsFileName = L"settings.json";
|
||||
|
||||
const static wchar_t* HotkeyID = L"hotkey";
|
||||
const static wchar_t* IncreaseOpacityHotkeyID = L"increase-opacity-hotkey";
|
||||
const static wchar_t* DecreaseOpacityHotkeyID = L"decrease-opacity-hotkey";
|
||||
const static wchar_t* SoundEnabledID = L"sound-enabled";
|
||||
const static wchar_t* ShowInSystemMenuID = L"show-in-system-menu";
|
||||
const static wchar_t* FrameEnabledID = L"frame-enabled";
|
||||
@@ -100,16 +102,21 @@ void AlwaysOnTopSettings::LoadSettings()
|
||||
const auto currentSettings = AlwaysOnTopSettings::settings();
|
||||
auto updatedSettings = std::make_shared<Settings>(*currentSettings);
|
||||
std::vector<SettingId> changedSettings;
|
||||
|
||||
if (const auto jsonVal = values.get_json(NonLocalizable::HotkeyID))
|
||||
{
|
||||
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
|
||||
if (updatedSettings->hotkey.get_modifiers() != val.get_modifiers() || updatedSettings->hotkey.get_key() != val.get_key() || updatedSettings->hotkey.get_code() != val.get_code())
|
||||
const auto updateHotkeySetting = [&](const wchar_t* hotkeyName, auto& currentHotkey, SettingId settingId) {
|
||||
if (const auto jsonVal = values.get_json(hotkeyName))
|
||||
{
|
||||
updatedSettings->hotkey = val;
|
||||
changedSettings.push_back(SettingId::Hotkey);
|
||||
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
|
||||
if (currentHotkey.get_modifiers() != val.get_modifiers() || currentHotkey.get_key() != val.get_key() || currentHotkey.get_code() != val.get_code())
|
||||
{
|
||||
currentHotkey = val;
|
||||
changedSettings.push_back(settingId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateHotkeySetting(NonLocalizable::HotkeyID, updatedSettings->hotkey, SettingId::Hotkey);
|
||||
updateHotkeySetting(NonLocalizable::IncreaseOpacityHotkeyID, updatedSettings->increaseOpacityHotkey, SettingId::IncreaseOpacityHotkey);
|
||||
updateHotkeySetting(NonLocalizable::DecreaseOpacityHotkeyID, updatedSettings->decreaseOpacityHotkey, SettingId::DecreaseOpacityHotkey);
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::SoundEnabledID))
|
||||
{
|
||||
|
||||
@@ -18,6 +18,8 @@ class SettingsObserver;
|
||||
struct Settings
|
||||
{
|
||||
PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T
|
||||
PowerToysSettings::HotkeyObject increaseOpacityHotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, VK_OEM_PLUS); // win + ctrl + '+'
|
||||
PowerToysSettings::HotkeyObject decreaseOpacityHotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, VK_OEM_MINUS); // win + ctrl + '-'
|
||||
static constexpr int minTransparencyPercentage = 20; // minimum transparency (can't go below 20%)
|
||||
static constexpr int maxTransparencyPercentage = 100; // maximum (fully opaque)
|
||||
static constexpr int transparencyStep = 10; // step size for +/- adjustment
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
enum class SettingId
|
||||
{
|
||||
Hotkey = 0,
|
||||
IncreaseOpacityHotkey,
|
||||
DecreaseOpacityHotkey,
|
||||
SoundEnabled,
|
||||
ShowInSystemMenu,
|
||||
FrameEnabled,
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const wchar_t ModulePath[] = L"PowerToys.AlwaysOnTop.exe";
|
||||
// Keep in sync with src\modules\alwaysontop\AlwaysOnTop\AlwaysOnTop.cpp
|
||||
const wchar_t PinnedWindowProp[] = L"AlwaysOnTop_Pinned";
|
||||
}
|
||||
|
||||
namespace
|
||||
@@ -27,6 +29,8 @@ namespace
|
||||
const wchar_t JSON_KEY_SHIFT[] = L"shift";
|
||||
const wchar_t JSON_KEY_CODE[] = L"code";
|
||||
const wchar_t JSON_KEY_HOTKEY[] = L"hotkey";
|
||||
const wchar_t JSON_KEY_INCREASE_OPACITY_HOTKEY[] = L"increase-opacity-hotkey";
|
||||
const wchar_t JSON_KEY_DECREASE_OPACITY_HOTKEY[] = L"decrease-opacity-hotkey";
|
||||
const wchar_t JSON_KEY_VALUE[] = L"value";
|
||||
}
|
||||
|
||||
@@ -107,27 +111,38 @@ public:
|
||||
|
||||
virtual bool on_hotkey(size_t hotkeyId) override
|
||||
{
|
||||
if (m_enabled)
|
||||
if (!m_enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
|
||||
|
||||
if (hotkeyId == 0)
|
||||
{
|
||||
Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
|
||||
if (!is_process_running())
|
||||
{
|
||||
Enable();
|
||||
}
|
||||
|
||||
if (hotkeyId == 0)
|
||||
SetEvent(m_hPinEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hotkeyId == 1 || hotkeyId == 2)
|
||||
{
|
||||
const HWND foregroundWindow = GetForegroundWindow();
|
||||
if (!foregroundWindow || !IsWindow(foregroundWindow) || !GetPropW(foregroundWindow, NonLocalizable::PinnedWindowProp))
|
||||
{
|
||||
SetEvent(m_hPinEvent);
|
||||
}
|
||||
else if (hotkeyId == 1)
|
||||
{
|
||||
SetEvent(m_hIncreaseOpacityEvent);
|
||||
}
|
||||
else if (hotkeyId == 2)
|
||||
{
|
||||
SetEvent(m_hDecreaseOpacityEvent);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_process_running())
|
||||
{
|
||||
Enable();
|
||||
}
|
||||
|
||||
SetEvent(hotkeyId == 1 ? m_hIncreaseOpacityEvent : m_hDecreaseOpacityEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -136,48 +151,48 @@ public:
|
||||
|
||||
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
||||
{
|
||||
size_t count = 0;
|
||||
|
||||
// Hotkey 0: Pin/Unpin (e.g., Win+Ctrl+T)
|
||||
if (m_hotkey.key)
|
||||
constexpr size_t hotkeyCount = 3;
|
||||
Hotkey configuredHotkeys[hotkeyCount] = { m_hotkey, m_increaseOpacityHotkey, m_decreaseOpacityHotkey };
|
||||
|
||||
for (size_t i = 0; i < hotkeyCount; ++i)
|
||||
{
|
||||
if (hotkeys && buffer_size > count)
|
||||
{
|
||||
hotkeys[count] = m_hotkey;
|
||||
Logger::trace(L"AlwaysOnTop hotkey[0]: win={}, ctrl={}, shift={}, alt={}, key={}",
|
||||
m_hotkey.win, m_hotkey.ctrl, m_hotkey.shift, m_hotkey.alt, m_hotkey.key);
|
||||
}
|
||||
count++;
|
||||
configuredHotkeys[i].id = static_cast<int>(i);
|
||||
configuredHotkeys[i].isShown = configuredHotkeys[i].key != 0;
|
||||
}
|
||||
|
||||
// Hotkey 1: Increase opacity (same modifiers + VK_OEM_PLUS '=')
|
||||
if (m_hotkey.key)
|
||||
if (hotkeys)
|
||||
{
|
||||
if (hotkeys && buffer_size > count)
|
||||
const size_t countToCopy = (buffer_size < hotkeyCount) ? buffer_size : hotkeyCount;
|
||||
for (size_t i = 0; i < countToCopy; ++i)
|
||||
{
|
||||
hotkeys[count] = m_hotkey;
|
||||
hotkeys[count].key = VK_OEM_PLUS; // '=' key
|
||||
Logger::trace(L"AlwaysOnTop hotkey[1] (increase opacity): win={}, ctrl={}, shift={}, alt={}, key={}",
|
||||
hotkeys[count].win, hotkeys[count].ctrl, hotkeys[count].shift, hotkeys[count].alt, hotkeys[count].key);
|
||||
hotkeys[i] = configuredHotkeys[i];
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
// Hotkey 2: Decrease opacity (same modifiers + VK_OEM_MINUS '-')
|
||||
if (m_hotkey.key)
|
||||
{
|
||||
if (hotkeys && buffer_size > count)
|
||||
{
|
||||
hotkeys[count] = m_hotkey;
|
||||
hotkeys[count].key = VK_OEM_MINUS; // '-' key
|
||||
Logger::trace(L"AlwaysOnTop hotkey[2] (decrease opacity): win={}, ctrl={}, shift={}, alt={}, key={}",
|
||||
hotkeys[count].win, hotkeys[count].ctrl, hotkeys[count].shift, hotkeys[count].alt, hotkeys[count].key);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
Logger::trace(L"AlwaysOnTop hotkey[0]: win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
|
||||
configuredHotkeys[0].win,
|
||||
configuredHotkeys[0].ctrl,
|
||||
configuredHotkeys[0].shift,
|
||||
configuredHotkeys[0].alt,
|
||||
configuredHotkeys[0].key,
|
||||
configuredHotkeys[0].isShown);
|
||||
Logger::trace(L"AlwaysOnTop hotkey[1] (increase opacity): win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
|
||||
configuredHotkeys[1].win,
|
||||
configuredHotkeys[1].ctrl,
|
||||
configuredHotkeys[1].shift,
|
||||
configuredHotkeys[1].alt,
|
||||
configuredHotkeys[1].key,
|
||||
configuredHotkeys[1].isShown);
|
||||
Logger::trace(L"AlwaysOnTop hotkey[2] (decrease opacity): win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
|
||||
configuredHotkeys[2].win,
|
||||
configuredHotkeys[2].ctrl,
|
||||
configuredHotkeys[2].shift,
|
||||
configuredHotkeys[2].alt,
|
||||
configuredHotkeys[2].key,
|
||||
configuredHotkeys[2].isShown);
|
||||
|
||||
Logger::trace(L"AlwaysOnTop get_hotkeys returning count={}", count);
|
||||
return count;
|
||||
Logger::trace(L"AlwaysOnTop get_hotkeys returning count={}", hotkeyCount);
|
||||
return hotkeyCount;
|
||||
}
|
||||
|
||||
// Enable the powertoy
|
||||
@@ -279,21 +294,34 @@ private:
|
||||
|
||||
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
const auto parseSingleHotkey = [](const winrt::Windows::Data::Json::JsonObject& propertiesObject, const wchar_t* hotkeyName, Hotkey& hotkey) {
|
||||
try
|
||||
{
|
||||
auto jsonHotkeyObject = propertiesObject.GetNamedObject(hotkeyName).GetNamedObject(JSON_KEY_VALUE);
|
||||
hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
|
||||
hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
|
||||
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
||||
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
||||
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
auto settingsObject = settings.get_raw_json();
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
try
|
||||
{
|
||||
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_HOTKEY).GetNamedObject(JSON_KEY_VALUE);
|
||||
m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
|
||||
m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
|
||||
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
||||
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
||||
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
||||
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
parseSingleHotkey(propertiesObject, JSON_KEY_HOTKEY, m_hotkey);
|
||||
parseSingleHotkey(propertiesObject, JSON_KEY_INCREASE_OPACITY_HOTKEY, m_increaseOpacityHotkey);
|
||||
parseSingleHotkey(propertiesObject, JSON_KEY_DECREASE_OPACITY_HOTKEY, m_decreaseOpacityHotkey);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to initialize AlwaysOnTop start shortcut");
|
||||
Logger::error("Failed to initialize AlwaysOnTop shortcuts");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -329,7 +357,9 @@ private:
|
||||
|
||||
bool m_enabled = false;
|
||||
HANDLE m_hProcess = nullptr;
|
||||
Hotkey m_hotkey;
|
||||
Hotkey m_hotkey{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = 'T' };
|
||||
Hotkey m_increaseOpacityHotkey{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = VK_OEM_PLUS };
|
||||
Hotkey m_decreaseOpacityHotkey{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = VK_OEM_MINUS };
|
||||
|
||||
// Handle to event used to pin/unpin windows
|
||||
HANDLE m_hPinEvent;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Shared constants for the extension load sentinel file used by
|
||||
/// <c>ProviderLoadGuard</c> and provider-specific crash sentinels to
|
||||
/// coordinate crash detection across process lifetimes.
|
||||
/// </summary>
|
||||
public static class ExtensionLoadState
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the sentinel JSON file written to the config directory.
|
||||
/// Both the app-level guard and individual extension sentinels must
|
||||
/// read and write the same file for crash detection to work.
|
||||
/// </summary>
|
||||
public const string SentinelFileName = "extensionLoadState.json";
|
||||
|
||||
/// <summary>
|
||||
/// JSON property name storing the owning provider id for a guarded block.
|
||||
/// </summary>
|
||||
public const string ProviderIdKey = "providerId";
|
||||
|
||||
/// <summary>
|
||||
/// JSON property name indicating a guarded block was active when the
|
||||
/// process exited.
|
||||
/// </summary>
|
||||
public const string LoadingKey = "loading";
|
||||
|
||||
/// <summary>
|
||||
/// JSON property name storing the consecutive crash count for a guarded
|
||||
/// block.
|
||||
/// </summary>
|
||||
public const string CrashCountKey = "crashCount";
|
||||
|
||||
/// <summary>
|
||||
/// Shared lock that must be held around every read-modify-write cycle
|
||||
/// on the sentinel file. Both <c>ProviderLoadGuard</c> and
|
||||
/// provider-specific crash sentinels run in the same process and would
|
||||
/// otherwise race on the file, silently dropping entries.
|
||||
/// </summary>
|
||||
public static readonly object SentinelFileLock = new();
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks guarded provider blocks in the shared extension-load sentinel file so
|
||||
/// callers can fail closed after repeated native crashes that bypass managed
|
||||
/// exception handling.
|
||||
/// </summary>
|
||||
public sealed class ProviderCrashSentinel
|
||||
{
|
||||
private readonly string _providerId;
|
||||
private readonly Lock _sentinelLock = new();
|
||||
private readonly HashSet<string> _completedBlocks = [];
|
||||
private readonly HashSet<string> _activeBlocks = [];
|
||||
|
||||
public ProviderCrashSentinel(string providerId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(providerId);
|
||||
_providerId = providerId;
|
||||
}
|
||||
|
||||
public bool BeginBlock(string blockSuffix)
|
||||
{
|
||||
var blockId = CreateBlockId(blockSuffix);
|
||||
|
||||
lock (_sentinelLock)
|
||||
{
|
||||
if (_completedBlocks.Contains(blockId) || !_activeBlocks.Add(blockId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateState(
|
||||
state =>
|
||||
{
|
||||
var entry = GetOrCreateEntry(state, blockId);
|
||||
entry[ExtensionLoadState.LoadingKey] = true;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void CompleteBlock(string blockSuffix)
|
||||
{
|
||||
var blockId = CreateBlockId(blockSuffix);
|
||||
|
||||
lock (_sentinelLock)
|
||||
{
|
||||
if (!_activeBlocks.Remove(blockId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_completedBlocks.Add(blockId);
|
||||
UpdateState(state => state.Remove(blockId));
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelBlock(string blockSuffix)
|
||||
{
|
||||
var blockId = CreateBlockId(blockSuffix);
|
||||
|
||||
lock (_sentinelLock)
|
||||
{
|
||||
if (!_activeBlocks.Remove(blockId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateState(state => state.Remove(blockId));
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearProviderState()
|
||||
{
|
||||
lock (_sentinelLock)
|
||||
{
|
||||
_completedBlocks.RemoveWhere(blockId => blockId.StartsWith(_providerId + ".", StringComparison.Ordinal));
|
||||
_activeBlocks.RemoveWhere(blockId => blockId.StartsWith(_providerId + ".", StringComparison.Ordinal));
|
||||
|
||||
UpdateState(
|
||||
state =>
|
||||
{
|
||||
var keysToRemove = state
|
||||
.Where(kvp => IsProviderEntry(kvp.Key, kvp.Value as JsonObject))
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToArray();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
state.Remove(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateState(Action<JsonObject> update)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (ExtensionLoadState.SentinelFileLock)
|
||||
{
|
||||
var sentinelPath = GetSentinelPath();
|
||||
var state = LoadState(sentinelPath);
|
||||
update(state);
|
||||
SaveState(sentinelPath, state);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError($"Failed to update crash sentinel state for provider '{_providerId}'.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject LoadState(string sentinelPath)
|
||||
{
|
||||
if (!File.Exists(sentinelPath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(sentinelPath);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
CoreLogger.LogWarning($"Crash sentinel state file '{sentinelPath}' was empty. Treating as empty state.");
|
||||
DeleteInvalidStateFile(sentinelPath);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (JsonNode.Parse(json) is JsonObject state)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
CoreLogger.LogError($"Crash sentinel state file '{sentinelPath}' did not contain a JSON object. Treating as empty state.");
|
||||
DeleteInvalidStateFile(sentinelPath);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
CoreLogger.LogError($"Failed to parse crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
|
||||
DeleteInvalidStateFile(sentinelPath);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
CoreLogger.LogError($"Failed to read crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
CoreLogger.LogError($"Access denied reading crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static void DeleteInvalidStateFile(string sentinelPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(sentinelPath);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
CoreLogger.LogError($"Failed to delete invalid crash sentinel state file '{sentinelPath}'.", ex);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
CoreLogger.LogError($"Access denied deleting invalid crash sentinel state file '{sentinelPath}'.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveState(string sentinelPath, JsonObject state)
|
||||
{
|
||||
if (state.Count == 0)
|
||||
{
|
||||
if (File.Exists(sentinelPath))
|
||||
{
|
||||
File.Delete(sentinelPath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(sentinelPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var tempPath = sentinelPath + ".tmp";
|
||||
File.WriteAllText(tempPath, state.ToJsonString());
|
||||
File.Move(tempPath, sentinelPath, overwrite: true);
|
||||
}
|
||||
|
||||
private JsonObject GetOrCreateEntry(JsonObject state, string blockId)
|
||||
{
|
||||
if (state[blockId] is JsonObject existing)
|
||||
{
|
||||
existing[ExtensionLoadState.ProviderIdKey] = _providerId;
|
||||
return existing;
|
||||
}
|
||||
|
||||
var entry = new JsonObject
|
||||
{
|
||||
[ExtensionLoadState.ProviderIdKey] = _providerId,
|
||||
[ExtensionLoadState.LoadingKey] = false,
|
||||
[ExtensionLoadState.CrashCountKey] = 0,
|
||||
};
|
||||
state[blockId] = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private bool IsProviderEntry(string blockId, JsonObject? entry)
|
||||
{
|
||||
var providerId = entry?[ExtensionLoadState.ProviderIdKey]?.GetValue<string>();
|
||||
if (string.Equals(providerId, _providerId, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return blockId.StartsWith(_providerId + ".", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string CreateBlockId(string blockSuffix)
|
||||
{
|
||||
return $"{_providerId}.{blockSuffix}";
|
||||
}
|
||||
|
||||
private static string GetSentinelPath()
|
||||
{
|
||||
return Path.Combine(Utilities.BaseSettingsPath("Microsoft.CmdPal"), ExtensionLoadState.SentinelFileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Reads the shared provider crash sentinel file at startup, increments crash
|
||||
/// counts for blocks left marked as active, and determines which providers
|
||||
/// should be soft-disabled for the current session.
|
||||
/// </summary>
|
||||
public sealed class ProviderLoadGuard
|
||||
{
|
||||
private const int MaxConsecutiveCrashes = 2;
|
||||
|
||||
private readonly string _sentinelPath;
|
||||
private readonly HashSet<string> _disabledProviders = [];
|
||||
|
||||
public ProviderLoadGuard(string configDirectory)
|
||||
{
|
||||
_sentinelPath = Path.Combine(configDirectory, ExtensionLoadState.SentinelFileName);
|
||||
DetectCrashes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the provider has been disabled due to repeated crashes
|
||||
/// in one of its tracked guarded blocks.
|
||||
/// </summary>
|
||||
public bool IsProviderDisabled(string providerId) => _disabledProviders.Contains(providerId);
|
||||
|
||||
/// <summary>
|
||||
/// Call immediately before attempting a guarded operation.
|
||||
/// Marks the block as "loading" in the sentinel file so that a
|
||||
/// subsequent native crash leaves evidence on disk.
|
||||
/// </summary>
|
||||
public void Enter(string blockId, string providerId)
|
||||
{
|
||||
UpdateState(state =>
|
||||
{
|
||||
var entry = GetOrCreateEntry(state, blockId, providerId);
|
||||
entry[ExtensionLoadState.LoadingKey] = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call after a guarded operation succeeds or fails gracefully via managed
|
||||
/// exception. Clears the loading flag and removes the block entry.
|
||||
/// </summary>
|
||||
public void Exit(string blockId)
|
||||
{
|
||||
UpdateState(state => state.Remove(blockId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes any persisted crash state for a provider so it can be retried
|
||||
/// on the next launch.
|
||||
/// </summary>
|
||||
public void ClearProvider(string providerId)
|
||||
{
|
||||
_disabledProviders.Remove(providerId);
|
||||
UpdateState(state =>
|
||||
{
|
||||
var keysToRemove = state
|
||||
.Where(kvp => TryGetProviderId(kvp.Key, kvp.Value as JsonObject) == providerId)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToArray();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
state.Remove(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void DetectCrashes()
|
||||
{
|
||||
// Read the sentinel file once at startup to detect providers that
|
||||
// crashed on the previous launch, then write back the updated state.
|
||||
lock (ExtensionLoadState.SentinelFileLock)
|
||||
{
|
||||
var state = ReadState();
|
||||
|
||||
var keysToCheck = state.Select(kvp => kvp.Key).ToArray();
|
||||
|
||||
foreach (var key in keysToCheck)
|
||||
{
|
||||
if (state[key] is not JsonObject entry)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var providerId = TryGetProviderId(key, entry);
|
||||
var wasLoading = entry[ExtensionLoadState.LoadingKey]?.GetValue<bool>() ?? false;
|
||||
|
||||
if (wasLoading)
|
||||
{
|
||||
// The guarded block was active when the process died.
|
||||
var crashCount = (entry[ExtensionLoadState.CrashCountKey]?.GetValue<int>() ?? 0) + 1;
|
||||
entry[ExtensionLoadState.CrashCountKey] = crashCount;
|
||||
entry[ExtensionLoadState.LoadingKey] = false;
|
||||
|
||||
if (crashCount >= MaxConsecutiveCrashes)
|
||||
{
|
||||
_disabledProviders.Add(providerId);
|
||||
CoreLogger.LogError($"Provider '{providerId}' disabled after {crashCount} consecutive crash(es) in guarded block '{key}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
CoreLogger.LogWarning($"Guarded block '{key}' for provider '{providerId}' crashed on previous launch (crash {crashCount}/{MaxConsecutiveCrashes}). Will retry.");
|
||||
}
|
||||
}
|
||||
|
||||
var currentCrashCount = entry[ExtensionLoadState.CrashCountKey]?.GetValue<int>() ?? 0;
|
||||
if (currentCrashCount >= MaxConsecutiveCrashes)
|
||||
{
|
||||
// Persist disabled state from a previous session.
|
||||
_disabledProviders.Add(providerId);
|
||||
}
|
||||
|
||||
if (!(entry[ExtensionLoadState.LoadingKey]?.GetValue<bool>() ?? false) && currentCrashCount == 0)
|
||||
{
|
||||
state.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
WriteState(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the sentinel file, applies a mutation, and writes it back
|
||||
/// under <see cref="ExtensionLoadState.SentinelFileLock"/> to prevent
|
||||
/// concurrent writers from clobbering each other's entries.
|
||||
/// </summary>
|
||||
private void UpdateState(Action<JsonObject> mutate)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (ExtensionLoadState.SentinelFileLock)
|
||||
{
|
||||
var state = ReadState();
|
||||
mutate(state);
|
||||
WriteState(state);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to update extension load sentinel file.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject GetOrCreateEntry(JsonObject state, string blockId, string providerId)
|
||||
{
|
||||
if (state[blockId] is JsonObject existing)
|
||||
{
|
||||
existing[ExtensionLoadState.ProviderIdKey] = providerId;
|
||||
return existing;
|
||||
}
|
||||
|
||||
var entry = new JsonObject
|
||||
{
|
||||
[ExtensionLoadState.ProviderIdKey] = providerId,
|
||||
[ExtensionLoadState.LoadingKey] = false,
|
||||
[ExtensionLoadState.CrashCountKey] = 0,
|
||||
};
|
||||
state[blockId] = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static string TryGetProviderId(string blockId, JsonObject? entry)
|
||||
{
|
||||
var providerId = entry?[ExtensionLoadState.ProviderIdKey]?.GetValue<string>();
|
||||
if (!string.IsNullOrWhiteSpace(providerId))
|
||||
{
|
||||
return providerId;
|
||||
}
|
||||
|
||||
var separatorIndex = blockId.IndexOf('.');
|
||||
return separatorIndex > 0 ? blockId[..separatorIndex] : blockId;
|
||||
}
|
||||
|
||||
private JsonObject ReadState()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_sentinelPath))
|
||||
{
|
||||
var json = File.ReadAllText(_sentinelPath);
|
||||
return JsonNode.Parse(json)?.AsObject() ?? [];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to read extension load sentinel file.", ex);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private void WriteState(JsonObject state)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (state.Count == 0)
|
||||
{
|
||||
if (File.Exists(_sentinelPath))
|
||||
{
|
||||
File.Delete(_sentinelPath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(_sentinelPath);
|
||||
if (directory != null)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var tempPath = _sentinelPath + ".tmp";
|
||||
File.WriteAllText(tempPath, state.ToJsonString());
|
||||
File.Move(tempPath, _sentinelPath, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to write extension load sentinel file.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"src\\common\\Common.Search\\Common.Search.csproj",
|
||||
"src\\common\\Common.UI\\Common.UI.csproj",
|
||||
"src\\common\\ManagedCommon\\ManagedCommon.csproj",
|
||||
"src\\common\\ManagedCsWin32\\ManagedCsWin32.csproj",
|
||||
"src\\common\\ManagedTelemetry\\Telemetry\\ManagedTelemetry.csproj",
|
||||
"src\\common\\PowerToys.ModuleContracts\\PowerToys.ModuleContracts.csproj",
|
||||
"src\\common\\SettingsAPI\\SettingsAPI.vcxproj",
|
||||
@@ -18,8 +19,11 @@
|
||||
"src\\modules\\ZoomIt\\ZoomItSettingsInterop\\ZoomItSettingsInterop.vcxproj",
|
||||
"src\\modules\\awake\\Awake.ModuleServices\\Awake.ModuleServices.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.PowerToys\\Microsoft.CmdPal.Ext.PowerToys.csproj",
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions.Toolkit\\Microsoft.CommandPalette.Extensions.Toolkit.csproj",
|
||||
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj",
|
||||
"src\\modules\\colorPicker\\ColorPicker.ModuleServices\\ColorPicker.ModuleServices.csproj",
|
||||
"src\\modules\\fancyzones\\FancyZonesEditorCommon\\FancyZonesEditorCommon.csproj",
|
||||
"src\\modules\\powerdisplay\\PowerDisplay.Lib\\PowerDisplay.Lib.csproj",
|
||||
"src\\settings-ui\\Settings.UI.Library\\Settings.UI.Library.csproj"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,35 +2,45 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class AliasManager : ObservableObject
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
// REMEMBER, CommandAlias.SearchPrefix is what we use as keys
|
||||
private readonly Dictionary<string, CommandAlias> _aliases;
|
||||
private static readonly ImmutableList<CommandAlias> _defaultAliases = new List<CommandAlias>
|
||||
{
|
||||
new CommandAlias(":", "com.microsoft.cmdpal.registry", true),
|
||||
new CommandAlias("$", "com.microsoft.cmdpal.windowsSettings", true),
|
||||
new CommandAlias("=", "com.microsoft.cmdpal.calculator", true),
|
||||
new CommandAlias(">", "com.microsoft.cmdpal.shell", true),
|
||||
new CommandAlias("<", "com.microsoft.cmdpal.windowwalker", true),
|
||||
new CommandAlias("??", "com.microsoft.cmdpal.websearch", true),
|
||||
new CommandAlias("file", "com.microsoft.indexer.fileSearch", false),
|
||||
new CommandAlias(")", "com.microsoft.cmdpal.timedate", true),
|
||||
}.ToImmutableList();
|
||||
|
||||
public AliasManager(TopLevelCommandManager tlcManager, SettingsModel settings)
|
||||
public AliasManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_aliases = settings.Aliases;
|
||||
_settingsService = settingsService;
|
||||
|
||||
if (_aliases.Count == 0)
|
||||
if (_settingsService.Settings.Aliases.Count == 0)
|
||||
{
|
||||
PopulateDefaultAliases();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAlias(CommandAlias a) => _aliases.Add(a.SearchPrefix, a);
|
||||
|
||||
public bool CheckAlias(string searchText)
|
||||
{
|
||||
if (_aliases.TryGetValue(searchText, out var alias))
|
||||
if (_settingsService.Settings.Aliases.TryGetValue(searchText, out var alias))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -53,19 +63,18 @@ public partial class AliasManager : ObservableObject
|
||||
|
||||
private void PopulateDefaultAliases()
|
||||
{
|
||||
this.AddAlias(new CommandAlias(":", "com.microsoft.cmdpal.registry", true));
|
||||
this.AddAlias(new CommandAlias("$", "com.microsoft.cmdpal.windowsSettings", true));
|
||||
this.AddAlias(new CommandAlias("=", "com.microsoft.cmdpal.calculator", true));
|
||||
this.AddAlias(new CommandAlias(">", "com.microsoft.cmdpal.shell", true));
|
||||
this.AddAlias(new CommandAlias("<", "com.microsoft.cmdpal.windowwalker", true));
|
||||
this.AddAlias(new CommandAlias("??", "com.microsoft.cmdpal.websearch", true));
|
||||
this.AddAlias(new CommandAlias("file", "com.microsoft.indexer.fileSearch", false));
|
||||
this.AddAlias(new CommandAlias(")", "com.microsoft.cmdpal.timedate", true));
|
||||
_settingsService.UpdateSettings(
|
||||
s => s with
|
||||
{
|
||||
Aliases = s.Aliases
|
||||
.AddRange(_defaultAliases.ToDictionary(a => a.SearchPrefix, a => a)),
|
||||
},
|
||||
hotReload: false);
|
||||
}
|
||||
|
||||
public string? KeysFromId(string commandId)
|
||||
{
|
||||
return _aliases
|
||||
return _settingsService.Settings.Aliases
|
||||
.Where(kv => kv.Value.CommandId == commandId)
|
||||
.Select(kv => kv.Value.Alias)
|
||||
.FirstOrDefault();
|
||||
@@ -73,7 +82,7 @@ public partial class AliasManager : ObservableObject
|
||||
|
||||
public CommandAlias? AliasFromId(string commandId)
|
||||
{
|
||||
return _aliases
|
||||
return _settingsService.Settings.Aliases
|
||||
.Where(kv => kv.Value.CommandId == commandId)
|
||||
.Select(kv => kv.Value)
|
||||
.FirstOrDefault();
|
||||
@@ -87,9 +96,11 @@ public partial class AliasManager : ObservableObject
|
||||
return;
|
||||
}
|
||||
|
||||
var aliases = _settingsService.Settings.Aliases;
|
||||
|
||||
// If we already have _this exact alias_, do nothing
|
||||
if (newAlias is not null &&
|
||||
_aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
||||
aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
||||
{
|
||||
if (existingAlias.CommandId == commandId)
|
||||
{
|
||||
@@ -97,19 +108,19 @@ public partial class AliasManager : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
List<CommandAlias> toRemove = [];
|
||||
foreach (var kv in _aliases)
|
||||
var keysToRemove = new List<string>();
|
||||
foreach (var kv in aliases)
|
||||
{
|
||||
// Look for the old aliases for the command, and remove it
|
||||
if (kv.Value.CommandId == commandId)
|
||||
{
|
||||
toRemove.Add(kv.Value);
|
||||
keysToRemove.Add(kv.Key);
|
||||
}
|
||||
|
||||
// Look for the alias belonging to another command, and remove it
|
||||
if (newAlias is not null && kv.Value.Alias == newAlias.Alias && kv.Value.CommandId != commandId)
|
||||
{
|
||||
toRemove.Add(kv.Value);
|
||||
keysToRemove.Add(kv.Key);
|
||||
|
||||
// Remove alias from other TopLevelViewModels it may be assigned to
|
||||
var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
|
||||
@@ -120,15 +131,16 @@ public partial class AliasManager : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var alias in toRemove)
|
||||
_settingsService.UpdateSettings(s =>
|
||||
{
|
||||
// REMEMBER, SearchPrefix is what we use as keys
|
||||
_aliases.Remove(alias.SearchPrefix);
|
||||
}
|
||||
var updatedAliases = s.Aliases.RemoveRange(keysToRemove);
|
||||
|
||||
if (newAlias is not null)
|
||||
{
|
||||
AddAlias(newAlias);
|
||||
}
|
||||
if (newAlias is not null)
|
||||
{
|
||||
updatedAliases = updatedAliases.Add(newAlias.SearchPrefix, newAlias);
|
||||
}
|
||||
|
||||
return s with { Aliases = updatedAliases };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class AppStateModel : ObservableObject
|
||||
public record AppStateModel
|
||||
{
|
||||
[JsonIgnore]
|
||||
public static readonly string FilePath;
|
||||
|
||||
public event TypedEventHandler<AppStateModel, object?>? StateChanged;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// STATE HERE
|
||||
// Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)!
|
||||
// Make sure that any new types you add are added to JsonSerializationContext!
|
||||
public RecentCommandsManager RecentCommands { get; set; } = new();
|
||||
public RecentCommandsManager RecentCommands { get; init; } = new();
|
||||
|
||||
public List<string> RunHistory { get; set; } = [];
|
||||
public ImmutableList<string> RunHistory { get; init; } = ImmutableList<string>.Empty;
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static AppStateModel()
|
||||
{
|
||||
FilePath = StateJsonPath();
|
||||
}
|
||||
|
||||
public static AppStateModel LoadState()
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
|
||||
}
|
||||
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
Debug.WriteLine("The provided settings file does not exist");
|
||||
return new();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
|
||||
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
|
||||
|
||||
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
|
||||
|
||||
return loaded ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.ToString());
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
public static void SaveState(AppStateModel model)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveState)}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Serialize the main dictionary to JSON and save it to the file
|
||||
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel!);
|
||||
|
||||
// validate JSON
|
||||
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
|
||||
{
|
||||
Logger.LogError("Failed to parse app state as a JsonObject.");
|
||||
return;
|
||||
}
|
||||
|
||||
// read previous settings
|
||||
if (!TryReadSavedState(out var savedSettings))
|
||||
{
|
||||
savedSettings = new JsonObject();
|
||||
}
|
||||
|
||||
// merge new settings into old ones
|
||||
foreach (var item in newSettings)
|
||||
{
|
||||
savedSettings[item.Key] = item.Value?.DeepClone();
|
||||
}
|
||||
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
// TODO: Instead of just raising the event here, we should
|
||||
// have a file change watcher on the settings file, and
|
||||
// reload the settings then
|
||||
model.StateChanged?.Invoke(model, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save application state to {FilePath}:", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadSavedState([NotNullWhen(true)] out JsonObject? savedSettings)
|
||||
{
|
||||
savedSettings = null;
|
||||
|
||||
// read existing content from the file
|
||||
string oldContent;
|
||||
try
|
||||
{
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
oldContent = File.ReadAllText(FilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// file doesn't exist (might not have been created yet), so consider this a success
|
||||
// and return empty settings
|
||||
savedSettings = new JsonObject();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to read app state file {FilePath}:\n{ex}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// detect empty file, just for sake of logging
|
||||
if (string.IsNullOrWhiteSpace(oldContent))
|
||||
{
|
||||
Logger.LogInfo($"App state file is empty: {FilePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// is it valid JSON?
|
||||
try
|
||||
{
|
||||
savedSettings = JsonNode.Parse(oldContent) as JsonObject;
|
||||
return savedSettings != null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to parse app state from {FilePath}:\n{ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string StateJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the settings is just next to the exe
|
||||
return Path.Combine(directory, "state.json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -87,7 +87,8 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
Color.FromArgb(255, 126, 115, 95), // #7e735f
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
@@ -100,33 +101,33 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_settings.Theme;
|
||||
get => (int)_settingsService.Settings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _settings.Theme;
|
||||
get => _settingsService.Settings.Theme;
|
||||
set
|
||||
{
|
||||
if (_settings.Theme != value)
|
||||
if (_settingsService.Settings.Theme != value)
|
||||
{
|
||||
_settings.Theme = value;
|
||||
_settingsService.UpdateSettings(s => s with { Theme = value });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _settings.ColorizationMode;
|
||||
get => _settingsService.Settings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_settings.ColorizationMode != value)
|
||||
if (_settingsService.Settings.ColorizationMode != value)
|
||||
{
|
||||
_settings.ColorizationMode = value;
|
||||
_settingsService.UpdateSettings(s => s with { ColorizationMode = value });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
@@ -145,25 +146,25 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_settings.ColorizationMode;
|
||||
get => (int)_settingsService.Settings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _settings.CustomThemeColor;
|
||||
get => _settingsService.Settings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_settings.CustomThemeColor != value)
|
||||
if (_settingsService.Settings.CustomThemeColor != value)
|
||||
{
|
||||
_settings.CustomThemeColor = value;
|
||||
_settingsService.UpdateSettings(s => s with { CustomThemeColor = value });
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
@@ -172,43 +173,43 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
ColorIntensity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _settings.CustomThemeColorIntensity;
|
||||
get => _settingsService.Settings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_settings.CustomThemeColorIntensity = value;
|
||||
_settingsService.UpdateSettings(s => s with { CustomThemeColorIntensity = value });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageTintIntensity
|
||||
{
|
||||
get => _settings.BackgroundImageTintIntensity;
|
||||
get => _settingsService.Settings.BackgroundImageTintIntensity;
|
||||
set
|
||||
{
|
||||
_settings.BackgroundImageTintIntensity = value;
|
||||
_settingsService.UpdateSettings(s => s with { BackgroundImageTintIntensity = value });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _settings.BackgroundImagePath ?? string.Empty;
|
||||
get => _settingsService.Settings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImagePath != value)
|
||||
if (_settingsService.Settings.BackgroundImagePath != value)
|
||||
{
|
||||
_settings.BackgroundImagePath = value;
|
||||
_settingsService.UpdateSettings(s => s with { BackgroundImagePath = value });
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
@@ -216,64 +217,64 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
BackgroundImageOpacity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _settings.BackgroundImageOpacity;
|
||||
get => _settingsService.Settings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageOpacity != value)
|
||||
if (_settingsService.Settings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_settings.BackgroundImageOpacity = value;
|
||||
_settingsService.UpdateSettings(s => s with { BackgroundImageOpacity = value });
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _settings.BackgroundImageBrightness;
|
||||
get => _settingsService.Settings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBrightness != value)
|
||||
if (_settingsService.Settings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_settings.BackgroundImageBrightness = value;
|
||||
_settingsService.UpdateSettings(s => s with { BackgroundImageBrightness = value });
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _settings.BackgroundImageBlurAmount;
|
||||
get => _settingsService.Settings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBlurAmount != value)
|
||||
if (_settingsService.Settings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_settings.BackgroundImageBlurAmount = value;
|
||||
_settingsService.UpdateSettings(s => s with { BackgroundImageBlurAmount = value });
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _settings.BackgroundImageFit;
|
||||
get => _settingsService.Settings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageFit != value)
|
||||
if (_settingsService.Settings.BackgroundImageFit != value)
|
||||
{
|
||||
_settings.BackgroundImageFit = value;
|
||||
_settingsService.UpdateSettings(s => s with { BackgroundImageFit = value });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,29 +300,29 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackdropOpacity
|
||||
{
|
||||
get => _settings.BackdropOpacity;
|
||||
get => _settingsService.Settings.BackdropOpacity;
|
||||
set
|
||||
{
|
||||
if (_settings.BackdropOpacity != value)
|
||||
if (_settingsService.Settings.BackdropOpacity != value)
|
||||
{
|
||||
_settings.BackdropOpacity = value;
|
||||
_settingsService.UpdateSettings(s => s with { BackdropOpacity = value });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveBackdropStyle));
|
||||
OnPropertyChanged(nameof(EffectiveImageOpacity));
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackdropStyleIndex
|
||||
{
|
||||
get => (int)_settings.BackdropStyle;
|
||||
get => (int)_settingsService.Settings.BackdropStyle;
|
||||
set
|
||||
{
|
||||
var newStyle = (BackdropStyle)value;
|
||||
if (_settings.BackdropStyle != newStyle)
|
||||
if (_settingsService.Settings.BackdropStyle != newStyle)
|
||||
{
|
||||
_settings.BackdropStyle = newStyle;
|
||||
_settingsService.UpdateSettings(s => s with { BackdropStyle = newStyle });
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
|
||||
@@ -334,7 +335,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
IsColorizationDetailsExpanded = false;
|
||||
}
|
||||
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,25 +344,25 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
/// Gets whether the backdrop opacity slider should be visible.
|
||||
/// </summary>
|
||||
public bool IsBackdropOpacityVisible =>
|
||||
BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
|
||||
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the backdrop description (for styles without options) should be visible.
|
||||
/// </summary>
|
||||
public bool IsMicaBackdropDescriptionVisible =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether background/colorization settings are available.
|
||||
/// </summary>
|
||||
public bool IsBackgroundSettingsEnabled =>
|
||||
BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
|
||||
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
|
||||
/// </summary>
|
||||
public bool IsBackgroundNotAvailableVisible =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
|
||||
|
||||
public BackdropStyle? EffectiveBackdropStyle
|
||||
{
|
||||
@@ -370,9 +371,9 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
// Return style when transparency/blur is visible (not fully opaque Acrylic)
|
||||
// - Clear/Mica/MicaAlt/AcrylicThin always show their effect
|
||||
// - Acrylic shows effect only when opacity < 100
|
||||
if (_settings.BackdropStyle != BackdropStyle.Acrylic || _settings.BackdropOpacity < 100)
|
||||
if (_settingsService.Settings.BackdropStyle != BackdropStyle.Acrylic || _settingsService.Settings.BackdropOpacity < 100)
|
||||
{
|
||||
return _settings.BackdropStyle;
|
||||
return _settingsService.Settings.BackdropStyle;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -381,39 +382,39 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public double EffectiveImageOpacity =>
|
||||
EffectiveBackdropStyle is not null
|
||||
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settings.BackdropOpacity / 100.0)
|
||||
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settingsService.Settings.BackdropOpacity / 100.0)
|
||||
: (BackgroundImageOpacity / 100f);
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
public bool IsCustomTintVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
|
||||
public bool IsColorIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public bool IsImageTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsImageTintIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective tint intensity for the preview, based on the current colorization mode.
|
||||
/// </summary>
|
||||
public int EffectiveTintIntensity => _settings.ColorizationMode is ColorizationMode.Image
|
||||
? _settings.BackgroundImageTintIntensity
|
||||
: _settings.CustomThemeColorIntensity;
|
||||
public int EffectiveTintIntensity => _settingsService.Settings.ColorizationMode is ColorizationMode.Image
|
||||
? _settingsService.Settings.BackgroundImageTintIntensity
|
||||
: _settingsService.Settings.CustomThemeColorIntensity;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsBackgroundControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
|
||||
public bool IsNoBackgroundVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
public bool IsAccentColorControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public bool IsResetButtonVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsResetButtonVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public BackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization
|
||||
? Colors.Transparent
|
||||
: ColorizationMode switch
|
||||
{
|
||||
@@ -428,7 +429,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsBackgroundImage
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsBackgroundImage
|
||||
? null
|
||||
: ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
@@ -436,11 +437,11 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, ISettingsService settingsService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
@@ -448,7 +449,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
|
||||
IsColorizationDetailsExpanded = _settingsService.Settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
@@ -467,9 +468,8 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Save()
|
||||
private void DebouncedReapply()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public class CommandAlias
|
||||
public record CommandAlias
|
||||
{
|
||||
public string CommandId { get; set; }
|
||||
public string CommandId { get; init; }
|
||||
|
||||
public string Alias { get; set; }
|
||||
public string Alias { get; init; }
|
||||
|
||||
public bool IsDirect { get; set; }
|
||||
public bool IsDirect { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string SearchPrefix => Alias + (IsDirect ? string.Empty : " ");
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
@@ -106,7 +105,8 @@ public sealed partial class CommandBarViewModel : ObservableObject,
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(SelectedItem.HasMoreCommands):
|
||||
case nameof(SelectedItem.CanOpenContextMenu):
|
||||
case nameof(SelectedItem.SecondaryCommand):
|
||||
UpdateContextItems();
|
||||
break;
|
||||
}
|
||||
@@ -122,9 +122,7 @@ public sealed partial class CommandBarViewModel : ObservableObject,
|
||||
}
|
||||
|
||||
SecondaryCommand = SelectedItem.SecondaryCommand;
|
||||
var moreCommands = SelectedItem.MoreCommands;
|
||||
|
||||
ShouldShowContextMenu = moreCommands.Count > 1 && SelectedItem.HasMoreCommands;
|
||||
ShouldShowContextMenu = SelectedItem.CanOpenContextMenu;
|
||||
|
||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||
OnPropertyChanged(nameof(SecondaryCommand));
|
||||
|
||||
@@ -86,6 +86,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand => _secondaryMoreCommand;
|
||||
|
||||
public bool CanOpenContextMenu => AllCommands.Any(item => item is CommandItemViewModel command && command.ShouldBeVisible);
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
@@ -213,22 +215,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
BuildAndInitMoreCommands();
|
||||
|
||||
if (!string.IsNullOrEmpty(model.Command?.Name))
|
||||
{
|
||||
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
|
||||
{
|
||||
_itemTitle = Name,
|
||||
Subtitle = Subtitle,
|
||||
Command = Command,
|
||||
|
||||
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
|
||||
// Anything we set manually here must stay in sync with the corresponding properties on CommandItemViewModel.
|
||||
};
|
||||
|
||||
// Only set the icon on the context item for us if our command didn't
|
||||
// have its own icon
|
||||
UpdateDefaultContextItemIcon();
|
||||
}
|
||||
TryCreateDefaultCommandContextItem(model);
|
||||
|
||||
lock (_moreCommandsLock)
|
||||
{
|
||||
@@ -238,6 +225,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
Initialized |= InitializedState.SelectionInitialized;
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands));
|
||||
UpdateProperty(nameof(CanOpenContextMenu));
|
||||
UpdateProperty(nameof(IsSelectedInitialized));
|
||||
}
|
||||
|
||||
@@ -335,14 +324,22 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
||||
_itemTitle = model.Title;
|
||||
|
||||
_defaultCommandContextItemViewModel?.Command = Command;
|
||||
_defaultCommandContextItemViewModel?.UpdateTitle(_itemTitle);
|
||||
UpdateDefaultContextItemIcon();
|
||||
if (_defaultCommandContextItemViewModel is not null)
|
||||
{
|
||||
_defaultCommandContextItemViewModel.Command = Command;
|
||||
_defaultCommandContextItemViewModel.UpdateTitle(_itemTitle);
|
||||
UpdateDefaultContextItemIcon();
|
||||
}
|
||||
else
|
||||
{
|
||||
TryCreateDefaultCommandContextItem(model);
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(Name));
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateProperty(nameof(Icon));
|
||||
UpdateProperty(nameof(HasText));
|
||||
UpdateProperty(nameof(CanOpenContextMenu));
|
||||
break;
|
||||
|
||||
case nameof(Title):
|
||||
@@ -374,7 +371,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
case nameof(model.MoreCommands):
|
||||
BuildAndInitMoreCommands();
|
||||
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands));
|
||||
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands), nameof(CanOpenContextMenu));
|
||||
|
||||
break;
|
||||
case nameof(DataPackage):
|
||||
@@ -402,8 +399,17 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
_itemTitle = model.Title;
|
||||
_titleCache.Invalidate();
|
||||
UpdateProperty(nameof(Title), nameof(Name));
|
||||
UpdateProperty(nameof(CanOpenContextMenu));
|
||||
|
||||
if (_defaultCommandContextItemViewModel is not null)
|
||||
{
|
||||
_defaultCommandContextItemViewModel.UpdateTitle(model.Command.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
TryCreateDefaultCommandContextItem(model);
|
||||
}
|
||||
|
||||
_defaultCommandContextItemViewModel?.UpdateTitle(model.Command.Name);
|
||||
break;
|
||||
|
||||
case nameof(Command.Icon):
|
||||
@@ -413,6 +419,46 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="_defaultCommandContextItemViewModel"/> when it does not exist
|
||||
/// yet and the current command has a non-empty name. This covers the case
|
||||
/// where an extension initially exposes a <c>NoOpCommand</c> (empty name)
|
||||
/// and later switches to a concrete command after <see cref="SlowInitializeProperties"/> has already run.
|
||||
/// When a new instance is created, the snapshot is refreshed and
|
||||
/// <see cref="AllCommands"/> is notified.
|
||||
/// </summary>
|
||||
private void TryCreateDefaultCommandContextItem(ICommandItem model)
|
||||
{
|
||||
if (_defaultCommandContextItemViewModel is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.Command?.Name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
|
||||
{
|
||||
_itemTitle = Name,
|
||||
Subtitle = Subtitle,
|
||||
Command = Command,
|
||||
|
||||
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
|
||||
// Anything we set manually here must stay in sync with the corresponding properties on CommandItemViewModel.
|
||||
};
|
||||
|
||||
UpdateDefaultContextItemIcon();
|
||||
|
||||
lock (_moreCommandsLock)
|
||||
{
|
||||
RefreshMoreCommandStateUnsafe();
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
}
|
||||
|
||||
private void UpdateDefaultContextItemIcon() =>
|
||||
|
||||
// Command icon takes precedence over our icon on the primary command
|
||||
@@ -491,6 +537,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(SecondaryCommand));
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
UpdateProperty(nameof(CanOpenContextMenu));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,8 @@ public partial class CommandPaletteContentPageViewModel : ContentPageViewModel
|
||||
IFormContent form => new ContentFormViewModel(form, context),
|
||||
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
|
||||
ITreeContent tree => new ContentTreeViewModel(tree, context),
|
||||
IPlainTextContent plainText => new ContentPlainTextViewModel(plainText, context),
|
||||
IImageContent image => new ContentImageViewModel(image, context),
|
||||
_ => null,
|
||||
};
|
||||
return viewModel;
|
||||
|
||||
@@ -127,7 +127,12 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
|
||||
private ProviderSettings GetProviderSettings(SettingsModel settings)
|
||||
{
|
||||
return settings.GetProviderSettings(this);
|
||||
if (!settings.ProviderSettings.TryGetValue(ProviderId, out var ps))
|
||||
{
|
||||
ps = new ProviderSettings();
|
||||
}
|
||||
|
||||
return ps.WithConnection(this);
|
||||
}
|
||||
|
||||
public async Task LoadTopLevelCommands(IServiceProvider serviceProvider)
|
||||
@@ -139,9 +144,27 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var providerSettings = GetProviderSettings(settingsService.Settings);
|
||||
|
||||
// Persist the connected provider settings (fallback commands, etc.)
|
||||
settingsService.UpdateSettings(
|
||||
s =>
|
||||
{
|
||||
if (!s.ProviderSettings.TryGetValue(ProviderId, out var ps))
|
||||
{
|
||||
ps = new ProviderSettings();
|
||||
}
|
||||
|
||||
var newPs = ps.WithConnection(this);
|
||||
|
||||
return s with
|
||||
{
|
||||
ProviderSettings = s.ProviderSettings.SetItem(ProviderId, newPs),
|
||||
};
|
||||
},
|
||||
hotReload: false);
|
||||
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
IsActive = providerSettings.IsEnabled;
|
||||
if (!IsActive)
|
||||
{
|
||||
@@ -249,16 +272,15 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
IServiceProvider serviceProvider,
|
||||
ICommandProvider4? four)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settings = serviceProvider.GetRequiredService<ISettingsService>().Settings;
|
||||
var contextMenuFactory = serviceProvider.GetService<IContextMenuFactory>()!;
|
||||
var state = serviceProvider.GetService<AppStateModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
var ourContext = GetProviderContext();
|
||||
WeakReference<IPageContext> pageContext = new(this.TopLevelPageContext);
|
||||
var make = (ICommandItem? i, TopLevelType t) =>
|
||||
{
|
||||
CommandItemViewModel commandItemViewModel = new(new(i), pageContext, contextMenuFactory: contextMenuFactory);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
|
||||
topLevelViewModel.InitializeProperties();
|
||||
|
||||
return topLevelViewModel;
|
||||
@@ -301,8 +323,19 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
var dockSettings = settings.DockSettings;
|
||||
var allPinnedCommands = dockSettings.AllPinnedCommands;
|
||||
var pinnedBandsForThisProvider = allPinnedCommands.Where(c => c.ProviderId == ProviderId);
|
||||
|
||||
// Track which command IDs we've already added to avoid duplicates
|
||||
// from settings that were pinned multiple times.
|
||||
HashSet<string> seenCommandIds = new(bands.Select(b => b.Id));
|
||||
|
||||
foreach (var (providerId, commandId) in pinnedBandsForThisProvider)
|
||||
{
|
||||
if (!seenCommandIds.Add(commandId))
|
||||
{
|
||||
Logger.LogWarning($"Skipping duplicate pinned dock band command {commandId} for provider {providerId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Looking for pinned dock band command {commandId} for provider {providerId}");
|
||||
|
||||
// First, try to lookup the command as one of this provider's
|
||||
@@ -407,59 +440,126 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
|
||||
public void PinCommand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var providerSettings = GetProviderSettings(settingsService.Settings);
|
||||
|
||||
if (!providerSettings.PinnedCommandIds.Contains(commandId))
|
||||
{
|
||||
providerSettings.PinnedCommandIds.Add(commandId);
|
||||
settingsService.UpdateSettings(
|
||||
s =>
|
||||
{
|
||||
if (!s.ProviderSettings.TryGetValue(ProviderId, out var ps))
|
||||
{
|
||||
ps = new ProviderSettings();
|
||||
}
|
||||
|
||||
var providerSettings = ps.WithConnection(this);
|
||||
var newPinned = providerSettings.PinnedCommandIds.Add(commandId);
|
||||
var newPs = providerSettings with { PinnedCommandIds = newPinned };
|
||||
|
||||
return s with
|
||||
{
|
||||
ProviderSettings = s.ProviderSettings.SetItem(ProviderId, newPs),
|
||||
};
|
||||
},
|
||||
hotReload: false);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
|
||||
if (providerSettings.PinnedCommandIds.Remove(commandId))
|
||||
{
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
settingsService.UpdateSettings(
|
||||
s =>
|
||||
{
|
||||
if (!s.ProviderSettings.TryGetValue(ProviderId, out var ps))
|
||||
{
|
||||
ps = new ProviderSettings();
|
||||
}
|
||||
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
}
|
||||
var providerSettings = ps.WithConnection(this);
|
||||
var newPinned = providerSettings.PinnedCommandIds.Remove(commandId);
|
||||
var newPs = providerSettings with { PinnedCommandIds = newPinned };
|
||||
|
||||
return s with
|
||||
{
|
||||
ProviderSettings = s.ProviderSettings.SetItem(ProviderId, newPs),
|
||||
};
|
||||
},
|
||||
hotReload: false);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
}
|
||||
|
||||
public void PinDockBand(string commandId, IServiceProvider serviceProvider)
|
||||
public void PinDockBand(string commandId, IServiceProvider serviceProvider, Dock.DockPinSide side = Dock.DockPinSide.Start, bool? showTitles = null, bool? showSubtitles = null)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
var settings = settingsService.Settings;
|
||||
var dockSettings = settings.DockSettings;
|
||||
|
||||
// Prevent duplicate pins — check all sections
|
||||
if (dockSettings.StartBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId) ||
|
||||
dockSettings.CenterBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId) ||
|
||||
dockSettings.EndBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId))
|
||||
{
|
||||
Logger.LogDebug($"Dock band '{commandId}' from provider '{this.ProviderId}' is already pinned; skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var bandSettings = new DockBandSettings
|
||||
{
|
||||
CommandId = commandId,
|
||||
ProviderId = this.ProviderId,
|
||||
ShowTitles = showTitles,
|
||||
ShowSubtitles = showSubtitles,
|
||||
};
|
||||
settings.DockSettings.StartBands.Add(bandSettings);
|
||||
|
||||
settingsService.UpdateSettings(
|
||||
s =>
|
||||
{
|
||||
var dockSettings = s.DockSettings;
|
||||
return s with
|
||||
{
|
||||
DockSettings = side switch
|
||||
{
|
||||
Dock.DockPinSide.Center => dockSettings with { CenterBands = dockSettings.CenterBands.Add(bandSettings) },
|
||||
Dock.DockPinSide.End => dockSettings with { EndBands = dockSettings.EndBands.Add(bandSettings) },
|
||||
_ => dockSettings with { StartBands = dockSettings.StartBands.Add(bandSettings) },
|
||||
},
|
||||
};
|
||||
},
|
||||
hotReload: false);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
}
|
||||
|
||||
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
settings.DockSettings.StartBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
settings.DockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
settings.DockSettings.EndBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
|
||||
settingsService.UpdateSettings(
|
||||
s =>
|
||||
{
|
||||
var dockSettings = s.DockSettings;
|
||||
return s with
|
||||
{
|
||||
DockSettings = dockSettings with
|
||||
{
|
||||
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId),
|
||||
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId),
|
||||
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId),
|
||||
},
|
||||
};
|
||||
},
|
||||
hotReload: false);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
}
|
||||
|
||||
public ICommandProviderContext GetProviderContext() => this;
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -42,8 +43,8 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
private readonly ThrottledDebouncedAction _refreshThrottledDebouncedAction;
|
||||
private readonly TopLevelCommandManager _tlcManager;
|
||||
private readonly AliasManager _aliasManager;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly AppStateModel _appStateModel;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IAppStateService _appStateService;
|
||||
private readonly ScoringFunction<IListItem> _scoringFunction;
|
||||
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
|
||||
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
|
||||
@@ -79,23 +80,23 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
|
||||
public MainListPage(
|
||||
TopLevelCommandManager topLevelCommandManager,
|
||||
SettingsModel settings,
|
||||
AliasManager aliasManager,
|
||||
AppStateModel appStateModel,
|
||||
IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||
IFuzzyMatcherProvider fuzzyMatcherProvider,
|
||||
ISettingsService settingsService,
|
||||
IAppStateService appStateService)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.home";
|
||||
Title = Resources.builtin_home_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
|
||||
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
|
||||
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
_aliasManager = aliasManager;
|
||||
_appStateModel = appStateModel;
|
||||
_appStateService = appStateService;
|
||||
_tlcManager = topLevelCommandManager;
|
||||
_fuzzyMatcherProvider = fuzzyMatcherProvider;
|
||||
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateModel.RecentCommands, _fuzzyMatcherProvider.Current);
|
||||
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settings.FallbackRanks);
|
||||
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateService.State.RecentCommands, _fuzzyMatcherProvider.Current);
|
||||
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settingsService.Settings.FallbackRanks);
|
||||
|
||||
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
|
||||
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
|
||||
@@ -150,8 +151,8 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
|
||||
|
||||
settings.SettingsChanged += SettingsChangedHandler;
|
||||
HotReloadSettings(settings);
|
||||
_settingsService.SettingsChanged += SettingsChangedHandler;
|
||||
HotReloadSettings(_settingsService.Settings);
|
||||
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
|
||||
|
||||
IsLoading = true;
|
||||
@@ -364,9 +365,9 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
}
|
||||
|
||||
// prefilter fallbacks
|
||||
var globalFallbacks = _settings.GetGlobalFallbacks();
|
||||
var specialFallbacks = new List<TopLevelViewModel>(globalFallbacks.Length);
|
||||
var commonFallbacks = new List<TopLevelViewModel>(commands.Count - globalFallbacks.Length);
|
||||
var configuredGlobalFallbackIds = _settingsService.Settings.GetGlobalFallbacks();
|
||||
var specialFallbacks = new List<TopLevelViewModel>(configuredGlobalFallbackIds.Length);
|
||||
var commonFallbacks = new List<TopLevelViewModel>(Math.Max(commands.Count - configuredGlobalFallbackIds.Length, 0));
|
||||
|
||||
foreach (var s in commands)
|
||||
{
|
||||
@@ -375,7 +376,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (globalFallbacks.Contains(s.Id))
|
||||
if (configuredGlobalFallbackIds.Contains(s.Id))
|
||||
{
|
||||
specialFallbacks.Add(s);
|
||||
}
|
||||
@@ -479,7 +480,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
|
||||
// We need to remove pinned apps from allNewApps so they don't show twice.
|
||||
// Pinned app command IDs are stored in ProviderSettings.PinnedCommandIds.
|
||||
_settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
|
||||
_settingsService.Settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
|
||||
var pinnedCommandIds = providerSettings?.PinnedCommandIds;
|
||||
|
||||
if (pinnedCommandIds is not null && pinnedCommandIds.Count > 0)
|
||||
@@ -508,7 +509,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && globalFallbacks.Contains(s.Id));
|
||||
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && configuredGlobalFallbackIds.Contains(s.Id));
|
||||
_scoredFallbackItems = InternalListHelpers.FilterListWithScores(newFallbacksForScoring, searchQuery, _scoringFunction);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
@@ -678,9 +679,10 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
public void UpdateHistory(IListItem topLevelOrAppItem)
|
||||
{
|
||||
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
||||
var history = _appStateModel.RecentCommands;
|
||||
history.AddHistoryItem(id);
|
||||
AppStateModel.SaveState(_appStateModel);
|
||||
_appStateService.UpdateState(state => state with
|
||||
{
|
||||
RecentCommands = state.RecentCommands.WithHistoryItem(id),
|
||||
});
|
||||
}
|
||||
|
||||
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
@@ -703,7 +705,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
RequestRefresh(fullRefresh: false);
|
||||
}
|
||||
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender);
|
||||
private void SettingsChangedHandler(ISettingsService sender, SettingsModel args) => HotReloadSettings(args);
|
||||
|
||||
private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails;
|
||||
|
||||
@@ -716,9 +718,9 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
_tlcManager.PropertyChanged -= TlcManager_PropertyChanged;
|
||||
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
|
||||
|
||||
if (_settings is not null)
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
_settings.SettingsChanged -= SettingsChangedHandler;
|
||||
_settingsService.SettingsChanged -= SettingsChangedHandler;
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
{
|
||||
private static readonly string _creatingText = "Creating new extension...";
|
||||
private readonly IExtensionTemplateService _extensionTemplateService;
|
||||
private readonly StatusMessage _creatingMessage = new()
|
||||
{
|
||||
Message = _creatingText,
|
||||
@@ -20,7 +21,15 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
};
|
||||
|
||||
public NewExtensionForm()
|
||||
: this(new ExtensionTemplateService())
|
||||
{
|
||||
}
|
||||
|
||||
private NewExtensionForm(IExtensionTemplateService extensionTemplateService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(extensionTemplateService);
|
||||
|
||||
_extensionTemplateService = extensionTemplateService;
|
||||
TemplateJson = $$"""
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
@@ -115,7 +124,7 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
|
||||
try
|
||||
{
|
||||
CreateExtension(extensionName, displayName, outputPath);
|
||||
_extensionTemplateService.CreateExtension(extensionName, displayName, outputPath);
|
||||
|
||||
BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage);
|
||||
|
||||
@@ -131,57 +140,6 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
private void CreateExtension(string extensionName, string newDisplayName, string outputPath)
|
||||
{
|
||||
var newGuid = Guid.NewGuid().ToString();
|
||||
|
||||
// Unzip `template.zip` to a temp dir:
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
|
||||
// Does the output path exist?
|
||||
if (!Directory.Exists(outputPath))
|
||||
{
|
||||
Directory.CreateDirectory(outputPath);
|
||||
}
|
||||
|
||||
var assetsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), "Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip");
|
||||
ZipFile.ExtractToDirectory(assetsPath, tempDir);
|
||||
|
||||
var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
|
||||
// Replace all the instances of `FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF` with a new random guid:
|
||||
text = text.Replace("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", newGuid);
|
||||
|
||||
// Then replace all the `TemplateCmdPalExtension` with `extensionName`
|
||||
text = text.Replace("TemplateCmdPalExtension", extensionName);
|
||||
|
||||
// Then replace all the `TemplateDisplayName` with `newDisplayName`
|
||||
text = text.Replace("TemplateDisplayName", newDisplayName);
|
||||
|
||||
// We're going to write the file to the same relative location in the output path
|
||||
var relativePath = Path.GetRelativePath(tempDir, file);
|
||||
|
||||
var newFileName = Path.Combine(outputPath, relativePath);
|
||||
|
||||
// if the file name had `TemplateCmdPalExtension` in it, replace it with `extensionName`
|
||||
newFileName = newFileName.Replace("TemplateCmdPalExtension", extensionName);
|
||||
|
||||
// Make sure the directory exists
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFileName)!);
|
||||
|
||||
File.WriteAllText(newFileName, text);
|
||||
|
||||
// Delete the old file
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
// Delete the temp dir
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
|
||||
private string FormatJsonString(string str) =>
|
||||
|
||||
// Escape the string for JSON
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ContentImageViewModel : ContentViewModel
|
||||
{
|
||||
public ExtensionObject<IImageContent> Model { get; }
|
||||
|
||||
public IconInfoViewModel Image { get; protected set; } = new(null);
|
||||
|
||||
public double MaxWidth { get; protected set; } = double.PositiveInfinity;
|
||||
|
||||
public double MaxHeight { get; protected set; } = double.PositiveInfinity;
|
||||
|
||||
public ContentImageViewModel(IImageContent content, WeakReference<IPageContext> context)
|
||||
: base(context)
|
||||
{
|
||||
Model = new ExtensionObject<IImageContent>(content);
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Image = new IconInfoViewModel(model.Image);
|
||||
Image.InitializeProperties();
|
||||
|
||||
MaxWidth = model.MaxWidth <= 0 ? double.PositiveInfinity : model.MaxWidth;
|
||||
MaxHeight = model.MaxHeight <= 0 ? double.PositiveInfinity : model.MaxHeight;
|
||||
|
||||
UpdateProperty(nameof(Image), nameof(MaxWidth), nameof(MaxHeight));
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var propName = args.PropertyName;
|
||||
FetchProperty(propName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Image):
|
||||
Image = new IconInfoViewModel(model.Image);
|
||||
Image.InitializeProperties();
|
||||
UpdateProperty(propertyName);
|
||||
break;
|
||||
|
||||
case nameof(IImageContent.MaxWidth):
|
||||
MaxWidth = model.MaxWidth <= 0 ? double.PositiveInfinity : model.MaxWidth;
|
||||
UpdateProperty(propertyName);
|
||||
break;
|
||||
|
||||
case nameof(IImageContent.MaxHeight):
|
||||
MaxHeight = model.MaxHeight <= 0 ? double.PositiveInfinity : model.MaxHeight;
|
||||
UpdateProperty(propertyName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
var model = Model.Unsafe;
|
||||
if (model is not null)
|
||||
{
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
|
||||
public bool HasMoreCommands => _snapshot.SecondaryCommand is not null;
|
||||
|
||||
public bool CanOpenContextMenu => _snapshot.AllCommands.Any(item => item is CommandItemViewModel command && command.ShouldBeVisible);
|
||||
|
||||
public string SecondaryCommandName => _snapshot.SecondaryCommand?.Name ?? string.Empty;
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand => _snapshot.PrimaryCommand;
|
||||
@@ -184,6 +186,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasCommands));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
UpdateProperty(nameof(CanOpenContextMenu));
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
DoOnUiThread(
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ContentPlainTextViewModel : ContentViewModel
|
||||
{
|
||||
private ExtensionObject<IPlainTextContent> Model { get; }
|
||||
|
||||
public string? Text { get; protected set; }
|
||||
|
||||
public bool WordWrapEnabled { get; protected set; }
|
||||
|
||||
public bool UseMonospace { get; protected set; }
|
||||
|
||||
public ContentPlainTextViewModel(IPlainTextContent content, WeakReference<IPageContext> context)
|
||||
: base(context)
|
||||
{
|
||||
Model = new ExtensionObject<IPlainTextContent>(content);
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Text = model.Text;
|
||||
WordWrapEnabled = model.WrapWords;
|
||||
UseMonospace = model.FontFamily == CommandPalette.Extensions.FontFamily.Monospace;
|
||||
UpdateProperty(nameof(Text), nameof(WordWrapEnabled), nameof(UseMonospace));
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var propName = args.PropertyName;
|
||||
FetchProperty(propName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = Model.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(IPlainTextContent.FontFamily):
|
||||
// RPC:
|
||||
var incomingUseMonospace = model.FontFamily == CommandPalette.Extensions.FontFamily.Monospace;
|
||||
|
||||
// local:
|
||||
if (incomingUseMonospace != UseMonospace)
|
||||
{
|
||||
UseMonospace = incomingUseMonospace;
|
||||
UpdateProperty(nameof(UseMonospace));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case nameof(IPlainTextContent.WrapWords):
|
||||
// RPC:
|
||||
var incomingWrap = model.WrapWords;
|
||||
|
||||
// local:
|
||||
if (WordWrapEnabled != incomingWrap)
|
||||
{
|
||||
WordWrapEnabled = model.WrapWords;
|
||||
UpdateProperty(nameof(WordWrapEnabled));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case nameof(IPlainTextContent.Text):
|
||||
// RPC:
|
||||
var incomingText = model.Text;
|
||||
|
||||
// local:
|
||||
if (incomingText != Text)
|
||||
{
|
||||
Text = incomingText;
|
||||
UpdateProperty(nameof(Text));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
var model = Model.Unsafe;
|
||||
if (model is not null)
|
||||
{
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,8 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
||||
IFormContent form => new ContentFormViewModel(form, context),
|
||||
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
|
||||
ITreeContent tree => new ContentTreeViewModel(tree, context),
|
||||
IPlainTextContent plainText => new ContentPlainTextViewModel(plainText, context),
|
||||
IImageContent image => new ContentImageViewModel(image, context),
|
||||
_ => null,
|
||||
};
|
||||
return viewModel;
|
||||
|
||||
@@ -47,7 +47,20 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
public ContextMenuViewModel(IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||
{
|
||||
_fuzzyMatcherProvider = fuzzyMatcherProvider;
|
||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||
}
|
||||
|
||||
public void HookCommandBar()
|
||||
{
|
||||
var messenger = WeakReferenceMessenger.Default;
|
||||
if (!messenger.IsRegistered<UpdateCommandBarMessage>(this))
|
||||
{
|
||||
messenger.Register<UpdateCommandBarMessage>(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnhookCommandBar()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Unregister<UpdateCommandBarMessage>(this);
|
||||
}
|
||||
|
||||
public void Receive(UpdateCommandBarMessage message)
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
@@ -12,11 +14,12 @@ namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
public partial class DockBandSettingsViewModel : ObservableObject
|
||||
{
|
||||
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly DockBandSettings _dockSettingsModel;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly TopLevelViewModel _adapter;
|
||||
private readonly DockBandViewModel? _bandViewModel;
|
||||
|
||||
private DockBandSettings _dockSettingsModel;
|
||||
|
||||
public string Title => _adapter.Title;
|
||||
|
||||
public string Description
|
||||
@@ -53,14 +56,14 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
if (value != _showLabels)
|
||||
{
|
||||
_showLabels = value;
|
||||
_dockSettingsModel.ShowLabels = value switch
|
||||
var newShowTitles = value switch
|
||||
{
|
||||
ShowLabelsOption.Default => null,
|
||||
ShowLabelsOption.Default => (bool?)null,
|
||||
ShowLabelsOption.ShowLabels => true,
|
||||
ShowLabelsOption.HideLabels => false,
|
||||
_ => null,
|
||||
};
|
||||
Save();
|
||||
UpdateModel(_dockSettingsModel with { ShowTitles = newShowTitles });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,19 +131,19 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
DockBandSettings dockSettingsModel,
|
||||
TopLevelViewModel topLevelAdapter,
|
||||
DockBandViewModel? bandViewModel,
|
||||
SettingsModel settingsModel)
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_dockSettingsModel = dockSettingsModel;
|
||||
_adapter = topLevelAdapter;
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsModel = settingsModel;
|
||||
_settingsService = settingsService;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
var inStart = dockSettings.StartBands.Any(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
if (inStart)
|
||||
{
|
||||
@@ -173,9 +176,38 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
return bandVm.Items.Count;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
private void UpdateModel(DockBandSettings newModel)
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
var commandId = _dockSettingsModel.CommandId;
|
||||
_settingsService.UpdateSettings(
|
||||
s =>
|
||||
{
|
||||
var dockSettings = s.DockSettings;
|
||||
return s with
|
||||
{
|
||||
DockSettings = dockSettings with
|
||||
{
|
||||
StartBands = ReplaceInList(dockSettings.StartBands, commandId, newModel),
|
||||
CenterBands = ReplaceInList(dockSettings.CenterBands, commandId, newModel),
|
||||
EndBands = ReplaceInList(dockSettings.EndBands, commandId, newModel),
|
||||
},
|
||||
};
|
||||
},
|
||||
hotReload: false);
|
||||
_dockSettingsModel = newModel;
|
||||
}
|
||||
|
||||
private static ImmutableList<DockBandSettings> ReplaceInList(ImmutableList<DockBandSettings> list, string commandId, DockBandSettings newModel)
|
||||
{
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (list[i].CommandId == commandId)
|
||||
{
|
||||
return list.SetItem(i, newModel);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void UpdatePinSide(DockPinSide value)
|
||||
@@ -188,44 +220,31 @@ public partial class DockBandSettingsViewModel : ObservableObject
|
||||
|
||||
public void SetBandPosition(DockPinSide side, int? index)
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var commandId = _dockSettingsModel.CommandId;
|
||||
|
||||
// Remove from all sides first
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
|
||||
// Add to the selected side
|
||||
switch (side)
|
||||
_settingsService.UpdateSettings(s =>
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.StartBands.Count;
|
||||
dockSettings.StartBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
var dockSettings = s.DockSettings;
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.CenterBands.Count;
|
||||
dockSettings.CenterBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
// Remove from all sides first
|
||||
var newDock = dockSettings with
|
||||
{
|
||||
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == commandId),
|
||||
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId),
|
||||
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == commandId),
|
||||
};
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.EndBands.Count;
|
||||
dockSettings.EndBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
// Add to the selected side
|
||||
newDock = side switch
|
||||
{
|
||||
DockPinSide.Start => newDock with { StartBands = newDock.StartBands.Insert(index ?? newDock.StartBands.Count, _dockSettingsModel) },
|
||||
DockPinSide.Center => newDock with { CenterBands = newDock.CenterBands.Insert(index ?? newDock.CenterBands.Count, _dockSettingsModel) },
|
||||
DockPinSide.End => newDock with { EndBands = newDock.EndBands.Insert(index ?? newDock.EndBands.Count, _dockSettingsModel) },
|
||||
_ => newDock,
|
||||
};
|
||||
|
||||
case DockPinSide.None:
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
Save();
|
||||
return s with { DockSettings = newDock };
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPinSideChanged(DockPinSide value)
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -15,11 +17,11 @@ namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
private readonly CommandItemViewModel _rootItem;
|
||||
private readonly DockBandSettings _bandSettings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly Action _saveSettings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
private DockBandSettings _bandSettings;
|
||||
|
||||
public ObservableCollection<DockItemViewModel> Items { get; } = new();
|
||||
|
||||
private bool _showTitles = true;
|
||||
@@ -103,8 +105,7 @@ public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
||||
/// </summary>
|
||||
internal void SaveShowLabels()
|
||||
{
|
||||
_bandSettings.ShowTitles = _showTitles;
|
||||
_bandSettings.ShowSubtitles = _showSubtitles;
|
||||
ReplaceBandInSettings(_bandSettings with { ShowTitles = _showTitles, ShowSubtitles = _showSubtitles });
|
||||
_showTitlesSnapshot = null;
|
||||
_showSubtitlesSnapshot = null;
|
||||
}
|
||||
@@ -127,21 +128,54 @@ public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private void ReplaceBandInSettings(DockBandSettings newSettings)
|
||||
{
|
||||
var commandId = _bandSettings.CommandId;
|
||||
_settingsService.UpdateSettings(
|
||||
s =>
|
||||
{
|
||||
var dockSettings = s.DockSettings;
|
||||
return s with
|
||||
{
|
||||
DockSettings = dockSettings with
|
||||
{
|
||||
StartBands = ReplaceBandInList(dockSettings.StartBands, commandId, newSettings),
|
||||
CenterBands = ReplaceBandInList(dockSettings.CenterBands, commandId, newSettings),
|
||||
EndBands = ReplaceBandInList(dockSettings.EndBands, commandId, newSettings),
|
||||
},
|
||||
};
|
||||
},
|
||||
false);
|
||||
_bandSettings = newSettings;
|
||||
}
|
||||
|
||||
private static ImmutableList<DockBandSettings> ReplaceBandInList(ImmutableList<DockBandSettings> list, string commandId, DockBandSettings newSettings)
|
||||
{
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (list[i].CommandId == commandId)
|
||||
{
|
||||
return list.SetItem(i, newSettings);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
internal DockBandViewModel(
|
||||
CommandItemViewModel commandItemViewModel,
|
||||
WeakReference<IPageContext> errorContext,
|
||||
DockBandSettings settings,
|
||||
DockSettings dockSettings,
|
||||
Action saveSettings,
|
||||
ISettingsService settingsService,
|
||||
IContextMenuFactory contextMenuFactory)
|
||||
: base(errorContext)
|
||||
{
|
||||
_rootItem = commandItemViewModel;
|
||||
_bandSettings = settings;
|
||||
_dockSettings = dockSettings;
|
||||
_saveSettings = saveSettings;
|
||||
_settingsService = settingsService;
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
|
||||
var dockSettings = settingsService.Settings.DockSettings;
|
||||
_showTitles = settings.ResolveShowTitles(dockSettings.ShowLabels);
|
||||
_showSubtitles = settings.ResolveShowSubtitles(dockSettings.ShowLabels);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -15,11 +17,12 @@ namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
public sealed partial class DockViewModel
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly DockPageContext _pageContext; // only to be used for our own context menu - not for dock bands themselves
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
private DockSettings _settings;
|
||||
private bool _isEditing;
|
||||
|
||||
public TaskScheduler Scheduler { get; }
|
||||
|
||||
@@ -34,13 +37,13 @@ public sealed partial class DockViewModel
|
||||
public DockViewModel(
|
||||
TopLevelCommandManager tlcManager,
|
||||
IContextMenuFactory contextMenuFactory,
|
||||
SettingsModel settings,
|
||||
TaskScheduler scheduler)
|
||||
TaskScheduler scheduler,
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
_settingsModel = settings;
|
||||
_settings = settings.DockSettings;
|
||||
_settingsService = settingsService;
|
||||
_settings = _settingsService.Settings.DockSettings;
|
||||
Scheduler = scheduler;
|
||||
_pageContext = new(this);
|
||||
|
||||
@@ -51,6 +54,12 @@ public sealed partial class DockViewModel
|
||||
|
||||
private void DockBands_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (_isEditing)
|
||||
{
|
||||
Logger.LogDebug("Skipping DockBands_CollectionChanged during edit mode");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Starting DockBands_CollectionChanged");
|
||||
SetupBands();
|
||||
Logger.LogDebug("Ended DockBands_CollectionChanged");
|
||||
@@ -72,7 +81,7 @@ public sealed partial class DockViewModel
|
||||
}
|
||||
|
||||
private void SetupBands(
|
||||
List<DockBandSettings> bands,
|
||||
ImmutableList<DockBandSettings> bands,
|
||||
ObservableCollection<DockBandViewModel> target)
|
||||
{
|
||||
List<DockBandViewModel> newBands = new();
|
||||
@@ -140,7 +149,7 @@ public sealed partial class DockViewModel
|
||||
DockBandSettings bandSettings,
|
||||
CommandItemViewModel commandItem)
|
||||
{
|
||||
DockBandViewModel band = new(commandItem, commandItem.PageContext, bandSettings, _settings, SaveSettings, _contextMenuFactory);
|
||||
DockBandViewModel band = new(commandItem, commandItem.PageContext, bandSettings, _settingsService, _contextMenuFactory);
|
||||
|
||||
// the band is NOT initialized here!
|
||||
return band;
|
||||
@@ -148,7 +157,7 @@ public sealed partial class DockViewModel
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = _settings });
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
||||
@@ -193,7 +202,7 @@ public sealed partial class DockViewModel
|
||||
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
@@ -205,20 +214,30 @@ public sealed partial class DockViewModel
|
||||
}
|
||||
|
||||
// Remove from all settings lists
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
||||
var newDock = dockSettings with
|
||||
{
|
||||
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId),
|
||||
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId),
|
||||
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId),
|
||||
};
|
||||
|
||||
// Add to target settings list at the correct index
|
||||
var targetSettings = targetSide switch
|
||||
var targetList = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => dockSettings.StartBands,
|
||||
DockPinSide.Center => dockSettings.CenterBands,
|
||||
DockPinSide.End => dockSettings.EndBands,
|
||||
_ => dockSettings.StartBands,
|
||||
DockPinSide.Start => newDock.StartBands,
|
||||
DockPinSide.Center => newDock.CenterBands,
|
||||
DockPinSide.End => newDock.EndBands,
|
||||
_ => newDock.StartBands,
|
||||
};
|
||||
var insertIndex = Math.Min(targetIndex, targetSettings.Count);
|
||||
targetSettings.Insert(insertIndex, bandSettings);
|
||||
var insertIndex = Math.Min(targetIndex, targetList.Count);
|
||||
newDock = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => newDock with { StartBands = targetList.Insert(insertIndex, bandSettings) },
|
||||
DockPinSide.Center => newDock with { CenterBands = targetList.Insert(insertIndex, bandSettings) },
|
||||
DockPinSide.End => newDock with { EndBands = targetList.Insert(insertIndex, bandSettings) },
|
||||
_ => newDock with { StartBands = targetList.Insert(insertIndex, bandSettings) },
|
||||
};
|
||||
_settings = newDock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -228,7 +247,7 @@ public sealed partial class DockViewModel
|
||||
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
@@ -240,10 +259,15 @@ public sealed partial class DockViewModel
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all sides (settings and UI)
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
||||
// Remove from all sides (settings)
|
||||
var newDock = dockSettings with
|
||||
{
|
||||
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId),
|
||||
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId),
|
||||
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId),
|
||||
};
|
||||
|
||||
// Remove from UI collections
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
@@ -253,8 +277,8 @@ public sealed partial class DockViewModel
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.StartBands.Count);
|
||||
dockSettings.StartBands.Insert(settingsIndex, bandSettings);
|
||||
var settingsIndex = Math.Min(targetIndex, newDock.StartBands.Count);
|
||||
newDock = newDock with { StartBands = newDock.StartBands.Insert(settingsIndex, bandSettings) };
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, StartItems.Count);
|
||||
StartItems.Insert(uiIndex, band);
|
||||
@@ -263,8 +287,8 @@ public sealed partial class DockViewModel
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.CenterBands.Count);
|
||||
dockSettings.CenterBands.Insert(settingsIndex, bandSettings);
|
||||
var settingsIndex = Math.Min(targetIndex, newDock.CenterBands.Count);
|
||||
newDock = newDock with { CenterBands = newDock.CenterBands.Insert(settingsIndex, bandSettings) };
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, CenterItems.Count);
|
||||
CenterItems.Insert(uiIndex, band);
|
||||
@@ -273,8 +297,8 @@ public sealed partial class DockViewModel
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.EndBands.Count);
|
||||
dockSettings.EndBands.Insert(settingsIndex, bandSettings);
|
||||
var settingsIndex = Math.Min(targetIndex, newDock.EndBands.Count);
|
||||
newDock = newDock with { EndBands = newDock.EndBands.Insert(settingsIndex, bandSettings) };
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, EndItems.Count);
|
||||
EndItems.Insert(uiIndex, band);
|
||||
@@ -282,6 +306,8 @@ public sealed partial class DockViewModel
|
||||
}
|
||||
}
|
||||
|
||||
_settings = newDock;
|
||||
|
||||
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
|
||||
}
|
||||
|
||||
@@ -297,17 +323,57 @@ public sealed partial class DockViewModel
|
||||
band.SaveShowLabels();
|
||||
}
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
// Preserve any per-band label edits made while in edit mode. Those edits are
|
||||
// saved independently of reorder, so merge the latest band settings back into
|
||||
// the local reordered snapshot before we persist dock settings.
|
||||
var latestBandSettings = BuildBandSettingsLookup(_settingsService.Settings.DockSettings);
|
||||
_settings = _settings with
|
||||
{
|
||||
StartBands = MergeBandSettings(_settings.StartBands, latestBandSettings),
|
||||
CenterBands = MergeBandSettings(_settings.CenterBands, latestBandSettings),
|
||||
EndBands = MergeBandSettings(_settings.EndBands, latestBandSettings),
|
||||
};
|
||||
|
||||
_snapshotDockSettings = null;
|
||||
_snapshotBandViewModels = null;
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
|
||||
// Save without hotReload to avoid triggering SettingsChanged → SetupBands,
|
||||
// which could race with stale DockBands_CollectionChanged work items and
|
||||
// re-add bands that were just unpinned.
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = _settings }, false);
|
||||
_isEditing = false;
|
||||
Logger.LogDebug("Saved band order to settings");
|
||||
}
|
||||
|
||||
private List<DockBandSettings>? _snapshotStartBands;
|
||||
private List<DockBandSettings>? _snapshotCenterBands;
|
||||
private List<DockBandSettings>? _snapshotEndBands;
|
||||
private static Dictionary<string, DockBandSettings> BuildBandSettingsLookup(DockSettings dockSettings)
|
||||
{
|
||||
var lookup = new Dictionary<string, DockBandSettings>(StringComparer.Ordinal);
|
||||
foreach (var band in dockSettings.StartBands.Concat(dockSettings.CenterBands).Concat(dockSettings.EndBands))
|
||||
{
|
||||
lookup[band.CommandId] = band;
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private static ImmutableList<DockBandSettings> MergeBandSettings(
|
||||
ImmutableList<DockBandSettings> targetBands,
|
||||
IReadOnlyDictionary<string, DockBandSettings> latestBandSettings)
|
||||
{
|
||||
var merged = targetBands;
|
||||
for (var i = 0; i < merged.Count; i++)
|
||||
{
|
||||
var commandId = merged[i].CommandId;
|
||||
if (latestBandSettings.TryGetValue(commandId, out var latestSettings))
|
||||
{
|
||||
merged = merged.SetItem(i, latestSettings);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private DockSettings? _snapshotDockSettings;
|
||||
private Dictionary<string, DockBandViewModel>? _snapshotBandViewModels;
|
||||
|
||||
/// <summary>
|
||||
@@ -316,13 +382,17 @@ public sealed partial class DockViewModel
|
||||
/// </summary>
|
||||
public void SnapshotBandOrder()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
_snapshotStartBands = dockSettings.StartBands.Select(b => b.Clone()).ToList();
|
||||
_snapshotCenterBands = dockSettings.CenterBands.Select(b => b.Clone()).ToList();
|
||||
_snapshotEndBands = dockSettings.EndBands.Select(b => b.Clone()).ToList();
|
||||
_isEditing = true;
|
||||
|
||||
var dockSettings = _settingsService.Settings.DockSettings;
|
||||
|
||||
var snapshotStartBandsCount = dockSettings.StartBands.Count;
|
||||
var snapshotCenterBandsCount = dockSettings.CenterBands.Count;
|
||||
var snapshotEndBandsCount = dockSettings.EndBands.Count;
|
||||
|
||||
// Snapshot band ViewModels so we can restore unpinned bands
|
||||
// Use a dictionary but handle potential duplicates gracefully
|
||||
_snapshotDockSettings = dockSettings;
|
||||
_snapshotBandViewModels = new Dictionary<string, DockBandViewModel>();
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
@@ -335,7 +405,7 @@ public sealed partial class DockViewModel
|
||||
band.SnapshotShowLabels();
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Snapshot taken: {_snapshotStartBands.Count} start bands, {_snapshotCenterBands.Count} center bands, {_snapshotEndBands.Count} end bands");
|
||||
Logger.LogDebug($"Snapshot taken: {snapshotStartBandsCount} start bands, {snapshotCenterBandsCount} center bands, {snapshotEndBandsCount} end bands");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -344,9 +414,7 @@ public sealed partial class DockViewModel
|
||||
/// </summary>
|
||||
public void RestoreBandOrder()
|
||||
{
|
||||
if (_snapshotStartBands == null ||
|
||||
_snapshotCenterBands == null ||
|
||||
_snapshotEndBands == null || _snapshotBandViewModels == null)
|
||||
if (_snapshotDockSettings == null || _snapshotBandViewModels == null)
|
||||
{
|
||||
Logger.LogWarning("No snapshot to restore from");
|
||||
return;
|
||||
@@ -358,38 +426,15 @@ public sealed partial class DockViewModel
|
||||
band.RestoreShowLabels();
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Restore settings from snapshot
|
||||
dockSettings.StartBands.Clear();
|
||||
dockSettings.CenterBands.Clear();
|
||||
dockSettings.EndBands.Clear();
|
||||
|
||||
foreach (var bandSnapshot in _snapshotStartBands)
|
||||
{
|
||||
var bandSettings = bandSnapshot.Clone();
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
foreach (var bandSnapshot in _snapshotCenterBands)
|
||||
{
|
||||
var bandSettings = bandSnapshot.Clone();
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
foreach (var bandSnapshot in _snapshotEndBands)
|
||||
{
|
||||
var bandSettings = bandSnapshot.Clone();
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
}
|
||||
// Restore settings from snapshot (immutable = just assign back)
|
||||
_settings = _snapshotDockSettings;
|
||||
|
||||
// Rebuild UI collections from restored settings using the snapshotted ViewModels
|
||||
RebuildUICollectionsFromSnapshot();
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotDockSettings = null;
|
||||
_snapshotBandViewModels = null;
|
||||
_isEditing = false;
|
||||
Logger.LogDebug("Restored band order from snapshot");
|
||||
}
|
||||
|
||||
@@ -400,7 +445,7 @@ public sealed partial class DockViewModel
|
||||
return;
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settings;
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
@@ -433,7 +478,7 @@ public sealed partial class DockViewModel
|
||||
|
||||
private void RebuildUICollections()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settings;
|
||||
|
||||
// Create a lookup of all current band ViewModels
|
||||
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
|
||||
@@ -510,7 +555,7 @@ public sealed partial class DockViewModel
|
||||
|
||||
// Create settings for the new band
|
||||
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId, ShowLabels = null };
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settings;
|
||||
|
||||
// Create the band view model
|
||||
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
|
||||
@@ -519,15 +564,15 @@ public sealed partial class DockViewModel
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
_settings = dockSettings with { StartBands = dockSettings.StartBands.Add(bandSettings) };
|
||||
StartItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.Center:
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
_settings = dockSettings with { CenterBands = dockSettings.CenterBands.Add(bandSettings) };
|
||||
CenterItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.End:
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
_settings = dockSettings with { EndBands = dockSettings.EndBands.Add(bandSettings) };
|
||||
EndItems.Add(bandVm);
|
||||
break;
|
||||
}
|
||||
@@ -550,12 +595,15 @@ public sealed partial class DockViewModel
|
||||
public void UnpinBand(DockBandViewModel band)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var dockSettings = _settings;
|
||||
|
||||
// Remove from settings
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
||||
_settings = dockSettings with
|
||||
{
|
||||
StartBands = dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId),
|
||||
CenterBands = dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId),
|
||||
EndBands = dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId),
|
||||
};
|
||||
|
||||
// Remove from UI collections
|
||||
StartItems.Remove(band);
|
||||
@@ -616,10 +664,10 @@ public sealed partial class DockViewModel
|
||||
|
||||
private void EmitDockConfiguration()
|
||||
{
|
||||
var isDockEnabled = _settingsModel.EnableDock;
|
||||
var isDockEnabled = _settingsService.Settings.EnableDock;
|
||||
var dockSide = isDockEnabled ? _settings.Side.ToString().ToLowerInvariant() : "none";
|
||||
|
||||
static string FormatBands(List<DockBandSettings> bands) =>
|
||||
static string FormatBands(ImmutableList<DockBandSettings> bands) =>
|
||||
string.Join("\n", bands.Select(b => $"{b.ProviderId}/{b.CommandId}"));
|
||||
|
||||
var startBands = isDockEnabled ? FormatBands(_settings.StartBands) : string.Empty;
|
||||
|
||||
@@ -23,8 +23,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
/// </summary>
|
||||
public sealed partial class DockAppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
@@ -37,54 +36,54 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_dockSettings.Theme;
|
||||
get => (int)_settingsService.Settings.DockSettings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _dockSettings.Theme;
|
||||
get => _settingsService.Settings.DockSettings.Theme;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Theme != value)
|
||||
if (_settingsService.Settings.DockSettings.Theme != value)
|
||||
{
|
||||
_dockSettings.Theme = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { Theme = value } });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackdropIndex
|
||||
{
|
||||
get => (int)_dockSettings.Backdrop;
|
||||
get => (int)_settingsService.Settings.DockSettings.Backdrop;
|
||||
set => Backdrop = (DockBackdrop)value;
|
||||
}
|
||||
|
||||
public DockBackdrop Backdrop
|
||||
{
|
||||
get => _dockSettings.Backdrop;
|
||||
get => _settingsService.Settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Backdrop != value)
|
||||
if (_settingsService.Settings.DockSettings.Backdrop != value)
|
||||
{
|
||||
_dockSettings.Backdrop = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { Backdrop = value } });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackdropIndex));
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _dockSettings.ColorizationMode;
|
||||
get => _settingsService.Settings.DockSettings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.ColorizationMode != value)
|
||||
if (_settingsService.Settings.DockSettings.ColorizationMode != value)
|
||||
{
|
||||
_dockSettings.ColorizationMode = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { ColorizationMode = value } });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
@@ -100,25 +99,25 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_dockSettings.ColorizationMode;
|
||||
get => (int)_settingsService.Settings.DockSettings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _dockSettings.CustomThemeColor;
|
||||
get => _settingsService.Settings.DockSettings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.CustomThemeColor != value)
|
||||
if (_settingsService.Settings.DockSettings.CustomThemeColor != value)
|
||||
{
|
||||
_dockSettings.CustomThemeColor = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { CustomThemeColor = value } });
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
@@ -127,30 +126,30 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
ColorIntensity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _dockSettings.CustomThemeColorIntensity;
|
||||
get => _settingsService.Settings.DockSettings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_dockSettings.CustomThemeColorIntensity = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { CustomThemeColorIntensity = value } });
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _dockSettings.BackgroundImagePath ?? string.Empty;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImagePath != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImagePath != value)
|
||||
{
|
||||
_dockSettings.BackgroundImagePath = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImagePath = value } });
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
@@ -158,64 +157,64 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
BackgroundImageOpacity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _dockSettings.BackgroundImageOpacity;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageOpacity != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageOpacity = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImageOpacity = value } });
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBrightness;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBrightness != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBrightness = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImageBrightness = value } });
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBlurAmount;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBlurAmount != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBlurAmount = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImageBlurAmount = value } });
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _dockSettings.BackgroundImageFit;
|
||||
get => _settingsService.Settings.DockSettings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageFit != value)
|
||||
if (_settingsService.Settings.DockSettings.BackgroundImageFit != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageFit = value;
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { BackgroundImageFit = value } });
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
DebouncedReapply();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,15 +236,15 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
public bool IsCustomTintVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
public bool IsCustomTintIntensityVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsBackgroundControlsVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _dockSettings.ColorizationMode is ColorizationMode.None;
|
||||
public bool IsNoBackgroundVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
public bool IsAccentColorControlsVisible => _settingsService.Settings.DockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
@@ -268,12 +267,11 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public DockAppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
public DockAppearanceSettingsViewModel(IThemeService themeService, ISettingsService settingsService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_dockSettings = settings.DockSettings;
|
||||
_settingsService = settingsService;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
@@ -281,7 +279,7 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _dockSettings.ColorizationMode != ColorizationMode.None;
|
||||
IsColorizationDetailsExpanded = _settingsService.Settings.DockSettings.ColorizationMode != ColorizationMode.None;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
@@ -300,9 +298,8 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Save()
|
||||
private void DebouncedReapply()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public class FallbackSettings
|
||||
public record FallbackSettings
|
||||
{
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
|
||||
public bool IncludeInGlobalResults { get; set; }
|
||||
public bool IncludeInGlobalResults { get; init; }
|
||||
|
||||
public FallbackSettings()
|
||||
{
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class FallbackSettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly FallbackSettings _fallbackSettings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ProviderSettingsViewModel _providerSettingsViewModel;
|
||||
|
||||
private FallbackSettings _fallbackSettings;
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
@@ -26,15 +29,18 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
{
|
||||
if (value != _fallbackSettings.IsEnabled)
|
||||
{
|
||||
_fallbackSettings.IsEnabled = value;
|
||||
var newSettings = _fallbackSettings with { IsEnabled = value };
|
||||
|
||||
if (!_fallbackSettings.IsEnabled)
|
||||
if (!newSettings.IsEnabled)
|
||||
{
|
||||
_fallbackSettings.IncludeInGlobalResults = false;
|
||||
newSettings = newSettings with { IncludeInGlobalResults = false };
|
||||
}
|
||||
|
||||
Save();
|
||||
_fallbackSettings = newSettings;
|
||||
_providerSettingsViewModel.UpdateFallbackSettings(Id, _fallbackSettings);
|
||||
|
||||
OnPropertyChanged(nameof(IsEnabled));
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,15 +52,18 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
{
|
||||
if (value != _fallbackSettings.IncludeInGlobalResults)
|
||||
{
|
||||
_fallbackSettings.IncludeInGlobalResults = value;
|
||||
var newSettings = _fallbackSettings with { IncludeInGlobalResults = value };
|
||||
|
||||
if (!_fallbackSettings.IsEnabled)
|
||||
if (!newSettings.IsEnabled)
|
||||
{
|
||||
_fallbackSettings.IsEnabled = true;
|
||||
newSettings = newSettings with { IsEnabled = true };
|
||||
}
|
||||
|
||||
Save();
|
||||
_fallbackSettings = newSettings;
|
||||
_providerSettingsViewModel.UpdateFallbackSettings(Id, _fallbackSettings);
|
||||
|
||||
OnPropertyChanged(nameof(IncludeInGlobalResults));
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,10 +71,11 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
public FallbackSettingsViewModel(
|
||||
TopLevelViewModel fallback,
|
||||
FallbackSettings fallbackSettings,
|
||||
SettingsModel settingsModel,
|
||||
ProviderSettingsViewModel providerSettings)
|
||||
ProviderSettingsViewModel providerSettings,
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_settings = settingsModel;
|
||||
_settingsService = settingsService;
|
||||
_providerSettingsViewModel = providerSettings;
|
||||
_fallbackSettings = fallbackSettings;
|
||||
|
||||
Id = fallback.Id;
|
||||
@@ -76,10 +86,4 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
Icon = new(fallback.InitialIcon);
|
||||
Icon.InitializeProperties();
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public record HistoryItem
|
||||
{
|
||||
public required string CommandId { get; set; }
|
||||
public required string CommandId { get; init; }
|
||||
|
||||
public required int Uses { get; set; }
|
||||
public required int Uses { get; init; }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -10,36 +11,28 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class HotkeyManager : ObservableObject
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly List<TopLevelHotkey> _commandHotkeys;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public HotkeyManager(TopLevelCommandManager tlcManager, SettingsModel settings)
|
||||
public HotkeyManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_commandHotkeys = settings.CommandHotkeys;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public void UpdateHotkey(string commandId, HotkeySettings? hotkey)
|
||||
{
|
||||
// If any of the commands were already bound to this hotkey, remove that
|
||||
foreach (var item in _commandHotkeys)
|
||||
_settingsService.UpdateSettings(s =>
|
||||
{
|
||||
if (item.Hotkey == hotkey)
|
||||
// Remove any command already bound to this hotkey, and remove old binding for this command
|
||||
var hotkeys = s.CommandHotkeys
|
||||
.RemoveAll(item => item.Hotkey == hotkey || item.CommandId == commandId);
|
||||
|
||||
if (hotkey is not null)
|
||||
{
|
||||
item.Hotkey = null;
|
||||
hotkeys = hotkeys.Add(new(hotkey, commandId));
|
||||
}
|
||||
}
|
||||
|
||||
_commandHotkeys.RemoveAll(item => item.Hotkey is null);
|
||||
|
||||
foreach (var item in _commandHotkeys)
|
||||
{
|
||||
if (item.CommandId == commandId)
|
||||
{
|
||||
_commandHotkeys.Remove(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_commandHotkeys.Add(new(hotkey, commandId));
|
||||
return s with { CommandHotkeys = hotkeys };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
if (addedCommand)
|
||||
{
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,6 +253,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
oldCommand?.SafeCleanup();
|
||||
|
||||
UpdateProperty(nameof(MoreCommands), nameof(AllCommands));
|
||||
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common;
|
||||
@@ -12,6 +13,7 @@ using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -20,9 +22,11 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
{
|
||||
public const int IncrementalRefresh = -2;
|
||||
|
||||
private static readonly IEqualityComparer<IListItem> VmCacheComparer = new ProxyReferenceEqualityComparer();
|
||||
|
||||
private readonly TaskFactory filterTaskFactory = new(new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler);
|
||||
|
||||
private readonly Dictionary<IListItem, ListItemViewModel> _vmCache = new(new ProxyReferenceEqualityComparer());
|
||||
private Dictionary<IListItem, ListItemViewModel> _vmCache = new(VmCacheComparer);
|
||||
|
||||
// TODO: Do we want a base "ItemsPageViewModel" for anything that's going to have items?
|
||||
|
||||
@@ -36,24 +40,33 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
private readonly ExtensionObject<IListPage> _model;
|
||||
|
||||
private readonly Lock _fetchStateLock = new();
|
||||
private readonly Lock _listLock = new();
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<ListViewModel, int>? _getItemsDepthByViewModel;
|
||||
|
||||
private InterlockedBoolean _isLoading;
|
||||
private bool _isFetching;
|
||||
private int _activeFetchCount;
|
||||
private int _latestFetchGeneration;
|
||||
private bool _deferredFetchRequested;
|
||||
private bool _deferredFetchKeepSelection = true;
|
||||
|
||||
public event TypedEventHandler<ListViewModel, ItemsUpdatedEventArgs>? ItemsUpdated;
|
||||
|
||||
public bool ShowEmptyContent =>
|
||||
IsInitialized &&
|
||||
FilteredItems.Count == 0 &&
|
||||
(!_isFetching) &&
|
||||
IsLoading == false;
|
||||
!IsFetching &&
|
||||
!IsLoading;
|
||||
|
||||
public bool IsGridView { get; private set; }
|
||||
|
||||
public IGridPropertiesViewModel? GridProperties { get; private set; }
|
||||
|
||||
private bool IsFetching => Volatile.Read(ref _activeFetchCount) > 0;
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public bool ShowDetails { get; private set; }
|
||||
@@ -123,8 +136,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
|
||||
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems(args.TotalItems == IncrementalRefresh);
|
||||
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
RequestFetch(args.TotalItems == IncrementalRefresh);
|
||||
}
|
||||
|
||||
protected override void OnSearchTextBoxUpdated(string searchTextBox)
|
||||
{
|
||||
@@ -132,9 +147,9 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
// something needs to change, by raising ItemsChanged.
|
||||
if (_isDynamic)
|
||||
{
|
||||
filterCancellationTokenSource?.Cancel();
|
||||
filterCancellationTokenSource?.Dispose();
|
||||
filterCancellationTokenSource = new CancellationTokenSource();
|
||||
CancelAndDisposeTokenSource(ref filterCancellationTokenSource);
|
||||
var filterCts = filterCancellationTokenSource = new CancellationTokenSource();
|
||||
var filterToken = filterCts.Token;
|
||||
|
||||
// Hop off to an exclusive scheduler background thread to update the
|
||||
// extension. We do this to ensure that all filter update requests
|
||||
@@ -144,7 +159,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
_ = filterTaskFactory.StartNew(
|
||||
() =>
|
||||
{
|
||||
filterCancellationTokenSource.Token.ThrowIfCancellationRequested();
|
||||
filterToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -161,7 +176,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
ShowException(ex, _model?.Unsafe?.Name);
|
||||
}
|
||||
},
|
||||
filterCancellationTokenSource.Token,
|
||||
filterToken,
|
||||
TaskCreationOptions.None,
|
||||
filterTaskFactory.Scheduler!);
|
||||
}
|
||||
@@ -199,9 +214,73 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
private void RequestFetch(bool keepSelection)
|
||||
{
|
||||
// Keep RPC GetItems work off the UI thread. If the provider raises
|
||||
// ItemsChanged while we're already on a background thread, stay on that
|
||||
// thread so same-thread reentrancy detection still works.
|
||||
if (IsCurrentThreadUiThread())
|
||||
{
|
||||
QueueObservedBackgroundFetch(() => RequestFetch(keepSelection), "Failed to request background fetch");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsGetItemsActiveOnCurrentThread())
|
||||
{
|
||||
lock (_fetchStateLock)
|
||||
{
|
||||
_deferredFetchRequested = true;
|
||||
_deferredFetchKeepSelection &= keepSelection;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
FetchItems(keepSelection);
|
||||
}
|
||||
|
||||
private void QueueDeferredFetchIfNeeded()
|
||||
{
|
||||
bool deferredFetchRequested;
|
||||
bool keepSelection;
|
||||
lock (_fetchStateLock)
|
||||
{
|
||||
deferredFetchRequested = _deferredFetchRequested;
|
||||
keepSelection = _deferredFetchKeepSelection;
|
||||
_deferredFetchRequested = false;
|
||||
_deferredFetchKeepSelection = true;
|
||||
}
|
||||
|
||||
if (deferredFetchRequested)
|
||||
{
|
||||
QueueObservedBackgroundFetch(() => FetchItems(keepSelection), "Failed to execute deferred fetch");
|
||||
}
|
||||
}
|
||||
|
||||
private static void QueueObservedBackgroundFetch(Action action, string logMessage)
|
||||
{
|
||||
_ = Task.Run(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(logMessage, ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
|
||||
private void FetchItems(bool keepSelection)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(!IsCurrentThreadUiThread(), "FetchItems should not run on the UI thread.");
|
||||
|
||||
// If this fetch should reset selection, remember that intent even if
|
||||
// a later incremental fetch cancels us.
|
||||
if (!keepSelection)
|
||||
@@ -209,34 +288,50 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
_forceFirstItemPending = true;
|
||||
}
|
||||
|
||||
// Cancel any previous FetchItems operation
|
||||
_fetchItemsCancellationTokenSource?.Cancel();
|
||||
_fetchItemsCancellationTokenSource?.Dispose();
|
||||
_fetchItemsCancellationTokenSource = new CancellationTokenSource();
|
||||
CancellationToken cancellationToken;
|
||||
int fetchGeneration;
|
||||
lock (_fetchStateLock)
|
||||
{
|
||||
// Cancel any previous FetchItems operation
|
||||
CancelAndDisposeTokenSource(ref _fetchItemsCancellationTokenSource);
|
||||
_fetchItemsCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
var cancellationToken = _fetchItemsCancellationTokenSource.Token;
|
||||
cancellationToken = _fetchItemsCancellationTokenSource.Token;
|
||||
fetchGeneration = Interlocked.Increment(ref _latestFetchGeneration);
|
||||
}
|
||||
|
||||
_isFetching = true;
|
||||
|
||||
// Collect all the items into new viewmodels
|
||||
List<ListItemViewModel> newViewModels = [];
|
||||
// Declared outside try so catch blocks can reference them
|
||||
List<ListItemViewModel> createdViewModels = [];
|
||||
var itemsTransferredToList = false;
|
||||
var fetchCountIncremented = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Check for cancellation before starting expensive operations
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
fetchCountIncremented = true;
|
||||
if (Interlocked.Increment(ref _activeFetchCount) == 1)
|
||||
{
|
||||
return;
|
||||
UpdateEmptyContent();
|
||||
}
|
||||
|
||||
var newItems = _model.Unsafe!.GetItems();
|
||||
ThrowIfFetchCanceledOrStale(fetchGeneration, cancellationToken);
|
||||
|
||||
// Check for cancellation after getting items from extension
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
IListItem[] newItems;
|
||||
try
|
||||
{
|
||||
return;
|
||||
EnterGetItemsScope();
|
||||
newItems = _model.Unsafe!.GetItems();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ExitGetItemsScope();
|
||||
}
|
||||
|
||||
ThrowIfFetchCanceledOrStale(fetchGeneration, cancellationToken);
|
||||
|
||||
// Collect all the items into new viewmodels
|
||||
List<ListItemViewModel> newViewModels = new(newItems.Length);
|
||||
var currentCache = ReadVmCache();
|
||||
var nextCache = new Dictionary<IListItem, ListItemViewModel>(newItems.Length, VmCacheComparer);
|
||||
var showsTitle = GridProperties?.ShowTitle ?? true;
|
||||
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
|
||||
var created = 0;
|
||||
@@ -250,17 +345,14 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for cancellation during item processing
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ThrowIfFetchCanceledOrStale(fetchGeneration, cancellationToken);
|
||||
|
||||
if (_vmCache.TryGetValue(item, out var existing))
|
||||
if (nextCache.TryGetValue(item, out var existing) || currentCache.TryGetValue(item, out existing))
|
||||
{
|
||||
existing.LayoutShowsTitle = showsTitle;
|
||||
existing.LayoutShowsSubtitle = showsSubtitle;
|
||||
newViewModels.Add(existing);
|
||||
nextCache[item] = existing;
|
||||
reused++;
|
||||
continue;
|
||||
}
|
||||
@@ -273,68 +365,65 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
viewModel.LayoutShowsTitle = showsTitle;
|
||||
viewModel.LayoutShowsSubtitle = showsSubtitle;
|
||||
|
||||
_vmCache[item] = viewModel;
|
||||
newViewModels.Add(viewModel);
|
||||
createdViewModels.Add(viewModel);
|
||||
nextCache[item] = viewModel;
|
||||
created++;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Our own stale/cancel checks throw OCE to stop the whole fetch
|
||||
// promptly. Only swallow item-local cancellations.
|
||||
ThrowIfFetchCanceledOrStale(fetchGeneration, cancellationToken);
|
||||
CoreLogger.LogDebug("Item load cancelled during fetch");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to load item:\n", ex + ToString());
|
||||
CoreLogger.LogError("Failed to load item:\n", ex);
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
CoreLogger.LogInfo($"[ListViewModel] FetchItems: {created} created, {reused} reused, {_vmCache.Count} cached");
|
||||
CoreLogger.LogInfo($"[ListViewModel] FetchItems: {created} created, {reused} reused, {nextCache.Count} cached");
|
||||
#endif
|
||||
|
||||
// Check for cancellation before initializing first twenty items
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ThrowIfFetchCanceledOrStale(fetchGeneration, cancellationToken);
|
||||
|
||||
var firstTwenty = newViewModels.Take(20);
|
||||
foreach (var item in firstTwenty)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ThrowIfFetchCanceledOrStale(fetchGeneration, cancellationToken);
|
||||
|
||||
item?.SafeInitializeProperties();
|
||||
}
|
||||
|
||||
// Cancel any ongoing search
|
||||
_cancellationTokenSource?.Cancel();
|
||||
// Cancel any ongoing property initialization for the previous list.
|
||||
CancelAndDisposeTokenSource(ref _cancellationTokenSource);
|
||||
|
||||
// Check for cancellation before updating the list
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ThrowIfFetchCanceledOrStale(fetchGeneration, cancellationToken);
|
||||
|
||||
List<ListItemViewModel> removedItems;
|
||||
lock (_listLock)
|
||||
lock (_fetchStateLock)
|
||||
{
|
||||
// Now that we have new ViewModels for everything from the
|
||||
// extension, smartly update our list of VMs
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems);
|
||||
ThrowIfFetchCanceledOrStale(fetchGeneration, cancellationToken);
|
||||
|
||||
_vmCache.Clear();
|
||||
foreach (var vm in newViewModels)
|
||||
lock (_listLock)
|
||||
{
|
||||
if (vm.Model.Unsafe is { } li)
|
||||
{
|
||||
_vmCache[li] = vm;
|
||||
}
|
||||
}
|
||||
// Now that we have new ViewModels for everything from the
|
||||
// extension, smartly update our list of VMs
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems);
|
||||
|
||||
// DO NOT ThrowIfCancellationRequested AFTER THIS! If you do,
|
||||
// you'll clean up list items that we've now transferred into
|
||||
// .Items
|
||||
PublishVmCache(nextCache);
|
||||
|
||||
// DO NOT ThrowIfCancellationRequested AFTER THIS! If you do,
|
||||
// you'll clean up list items that we've now transferred into
|
||||
// .Items
|
||||
}
|
||||
}
|
||||
|
||||
itemsTransferredToList = true;
|
||||
|
||||
// If we removed items, we need to clean them up, to remove our event handlers
|
||||
foreach (var removedItem in removedItems)
|
||||
{
|
||||
@@ -348,9 +437,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
// However, if we were cancelled, we didn't actually add these items to
|
||||
// our Items list. Before we release them to the GC, make sure we clean
|
||||
// them up
|
||||
foreach (var vm in newViewModels)
|
||||
if (!itemsTransferredToList)
|
||||
{
|
||||
vm.SafeCleanup();
|
||||
foreach (var vm in createdViewModels)
|
||||
{
|
||||
vm.SafeCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -359,52 +451,83 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
{
|
||||
// TODO: Move this within the for loop, so we can catch issues with individual items
|
||||
// Create a special ListItemViewModel for errors and use an ItemTemplateSelector in the ListPage to display error items differently.
|
||||
if (!itemsTransferredToList)
|
||||
{
|
||||
foreach (var vm in createdViewModels)
|
||||
{
|
||||
vm.SafeCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
ShowException(ex, _model?.Unsafe?.Name);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isFetching = false;
|
||||
if (fetchCountIncremented && Interlocked.Decrement(ref _activeFetchCount) == 0)
|
||||
{
|
||||
UpdateEmptyContent();
|
||||
}
|
||||
}
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
var initializeItemsCts = new CancellationTokenSource();
|
||||
_cancellationTokenSource = initializeItemsCts;
|
||||
var initializeItemsToken = initializeItemsCts.Token;
|
||||
|
||||
_initializeItemsTask = new Task(() =>
|
||||
{
|
||||
InitializeItemsTask(_cancellationTokenSource.Token);
|
||||
InitializeItemsTask(initializeItemsToken);
|
||||
});
|
||||
_initializeItemsTask.Start();
|
||||
|
||||
DoOnUiThread(
|
||||
() =>
|
||||
{
|
||||
lock (_listLock)
|
||||
lock (_fetchStateLock)
|
||||
{
|
||||
// Now that our Items contains everything we want, it's time for us to
|
||||
// re-evaluate our Filter on those items.
|
||||
if (!_isDynamic)
|
||||
if (!IsLatestFetchGeneration(fetchGeneration))
|
||||
{
|
||||
// A static list? Great! Just run the filter.
|
||||
ApplyFilterUnderLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
// A dynamic list? Even better! Just stick everything into
|
||||
// FilteredItems. The extension already did any filtering it cared about.
|
||||
var snapshot = Items.Where(i => !i.IsInErrorState).ToList();
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateEmptyContent();
|
||||
lock (_listLock)
|
||||
{
|
||||
if (!IsLatestFetchGeneration(fetchGeneration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Now that our Items contains everything we want, it's time for us to
|
||||
// re-evaluate our Filter on those items.
|
||||
if (!_isDynamic)
|
||||
{
|
||||
// A static list? Great! Just run the filter.
|
||||
ApplyFilterUnderLock();
|
||||
}
|
||||
else
|
||||
{
|
||||
// A dynamic list? Even better! Just stick everything into
|
||||
// FilteredItems. The extension already did any filtering it cared about.
|
||||
var snapshot = Items.Where(i => !i.IsInErrorState).ToList();
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, snapshot);
|
||||
}
|
||||
|
||||
UpdateEmptyContent();
|
||||
}
|
||||
|
||||
if (!IsLatestFetchGeneration(fetchGeneration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume the pending flag on the UI thread so a
|
||||
// forceFirstItem=true intent survives cancellation.
|
||||
var forceFirst = _forceFirstItemPending;
|
||||
_forceFirstItemPending = false;
|
||||
|
||||
ItemsUpdated?.Invoke(this, new ItemsUpdatedEventArgs(forceFirstItem: IsRootPage && forceFirst));
|
||||
_isLoading.Clear();
|
||||
}
|
||||
|
||||
// Consume the pending flag on the UI thread so a
|
||||
// forceFirstItem=true intent survives cancellation.
|
||||
var forceFirst = _forceFirstItemPending;
|
||||
_forceFirstItemPending = false;
|
||||
|
||||
ItemsUpdated?.Invoke(this, new ItemsUpdatedEventArgs(forceFirstItem: IsRootPage && forceFirst));
|
||||
_isLoading.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -449,6 +572,105 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
/// </summary>
|
||||
private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, SearchTextBox));
|
||||
|
||||
private Dictionary<IListItem, ListItemViewModel> ReadVmCache() => Volatile.Read(ref _vmCache);
|
||||
|
||||
private static bool IsCurrentThreadUiThread()
|
||||
{
|
||||
try
|
||||
{
|
||||
return DispatcherQueue.GetForCurrentThread()?.HasThreadAccess == true;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects if we're currently within a GetItems call on this thread for this view model. This is used to detect
|
||||
/// reentrant calls to GetItems, so we can defer subsequent calls until the first one finishes, to avoid
|
||||
/// concurrent GetItems calls which most extensions won't be expecting.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if we're currently within a GetItems call on this thread for this view model; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
private bool IsGetItemsActiveOnCurrentThread()
|
||||
{
|
||||
var depths = _getItemsDepthByViewModel;
|
||||
return depths is not null &&
|
||||
depths.TryGetValue(this, out var depth) &&
|
||||
depth > 0;
|
||||
}
|
||||
|
||||
private void EnterGetItemsScope()
|
||||
{
|
||||
var depths = _getItemsDepthByViewModel ??= [];
|
||||
depths.TryGetValue(this, out var depth);
|
||||
depths[this] = depth + 1;
|
||||
}
|
||||
|
||||
private void ExitGetItemsScope()
|
||||
{
|
||||
var depths = _getItemsDepthByViewModel;
|
||||
if (depths is null || !depths.TryGetValue(this, out var depth))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth == 1)
|
||||
{
|
||||
depths.Remove(this);
|
||||
if (depths.Count == 0)
|
||||
{
|
||||
_getItemsDepthByViewModel = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
QueueDeferredFetchIfNeeded();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to queue deferred fetch", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
depths[this] = depth - 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CancelAndDisposeTokenSource(ref CancellationTokenSource? tokenSource)
|
||||
{
|
||||
var tokenSourceToDispose = Interlocked.Exchange(ref tokenSource, null);
|
||||
if (tokenSourceToDispose is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tokenSourceToDispose.Cancel();
|
||||
tokenSourceToDispose.Dispose();
|
||||
}
|
||||
|
||||
private void ThrowIfFetchCanceledOrStale(int fetchGeneration, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (Volatile.Read(ref _latestFetchGeneration) != fetchGeneration)
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsLatestFetchGeneration(int fetchGeneration)
|
||||
{
|
||||
return Volatile.Read(ref _latestFetchGeneration) == fetchGeneration;
|
||||
}
|
||||
|
||||
private void PublishVmCache(Dictionary<IListItem, ListItemViewModel> newCache)
|
||||
{
|
||||
Volatile.Write(ref _vmCache, newCache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to generate a weighting for a given list item, based on title,
|
||||
/// subtitle, etc. Largely a copy of the version in ListHelpers, but
|
||||
@@ -548,7 +770,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
// Cancel any in-flight slow init from a previous selection and defer
|
||||
// the expensive work (extension IPC for MoreCommands, details) so
|
||||
// rapid arrow-key navigation skips intermediate items entirely.
|
||||
_selectedItemCts?.Cancel();
|
||||
CancelAndDisposeTokenSource(ref _selectedItemCts);
|
||||
var cts = _selectedItemCts = new CancellationTokenSource();
|
||||
var ct = cts.Token;
|
||||
|
||||
@@ -634,7 +856,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
private void ClearSelectedItem()
|
||||
{
|
||||
_selectedItemCts?.Cancel();
|
||||
CancelAndDisposeTokenSource(ref _selectedItemCts);
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
@@ -820,21 +1042,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
|
||||
filterCancellationTokenSource?.Cancel();
|
||||
filterCancellationTokenSource?.Dispose();
|
||||
filterCancellationTokenSource = null;
|
||||
|
||||
_fetchItemsCancellationTokenSource?.Cancel();
|
||||
_fetchItemsCancellationTokenSource?.Dispose();
|
||||
_fetchItemsCancellationTokenSource = null;
|
||||
|
||||
_selectedItemCts?.Cancel();
|
||||
_selectedItemCts?.Dispose();
|
||||
_selectedItemCts = null;
|
||||
CancelAndDisposeTokenSource(ref _cancellationTokenSource);
|
||||
CancelAndDisposeTokenSource(ref filterCancellationTokenSource);
|
||||
CancelAndDisposeTokenSource(ref _fetchItemsCancellationTokenSource);
|
||||
CancelAndDisposeTokenSource(ref _selectedItemCts);
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
@@ -844,10 +1055,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
EmptyContent?.SafeCleanup();
|
||||
EmptyContent = new(new(null), PageContext, contextMenuFactory: null); // necessary?
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
filterCancellationTokenSource?.Cancel();
|
||||
_fetchItemsCancellationTokenSource?.Cancel();
|
||||
_selectedItemCts?.Cancel();
|
||||
CancelAndDisposeTokenSource(ref _cancellationTokenSource);
|
||||
CancelAndDisposeTokenSource(ref filterCancellationTokenSource);
|
||||
CancelAndDisposeTokenSource(ref _fetchItemsCancellationTokenSource);
|
||||
CancelAndDisposeTokenSource(ref _selectedItemCts);
|
||||
|
||||
lock (_listLock)
|
||||
{
|
||||
@@ -865,6 +1076,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
FilteredItems.Clear();
|
||||
}
|
||||
|
||||
PublishVmCache(new(VmCacheComparer));
|
||||
|
||||
Filters?.PropertyChanged -= FiltersPropertyChanged;
|
||||
Filters?.SafeCleanup();
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record PinToDockMessage(string ProviderId, string CommandId, bool Pin);
|
||||
public record PinToDockMessage(
|
||||
string ProviderId,
|
||||
string CommandId,
|
||||
bool Pin,
|
||||
DockPinSide Side = DockPinSide.Start,
|
||||
bool? ShowTitles = null,
|
||||
bool? ShowSubtitles = null);
|
||||
|
||||
@@ -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 Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ShowPinToDockDialogMessage(
|
||||
string ProviderId,
|
||||
string CommandId,
|
||||
string Title,
|
||||
string Subtitle,
|
||||
IconInfoViewModel? Icon,
|
||||
DockSide DockSide);
|
||||
@@ -22,6 +22,8 @@ public interface IContextMenuContext : INotifyPropertyChanged
|
||||
|
||||
public bool HasMoreCommands { get; }
|
||||
|
||||
public bool CanOpenContextMenu { get; }
|
||||
|
||||
public IReadOnlyList<IContextItemViewModel> AllCommands { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,37 +2,39 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public class ProviderSettings
|
||||
public record ProviderSettings
|
||||
{
|
||||
// List of built-in fallbacks that should not have global results enabled by default
|
||||
private readonly string[] _excludedBuiltInFallbacks = [
|
||||
private static readonly string[] _excludedBuiltInFallbacks = [
|
||||
"com.microsoft.cmdpal.builtin.indexer.fallback",
|
||||
"com.microsoft.cmdpal.builtin.calculator.fallback",
|
||||
"com.microsoft.cmdpal.builtin.remotedesktop.fallback",
|
||||
];
|
||||
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
|
||||
public Dictionary<string, FallbackSettings> FallbackCommands { get; set; } = new();
|
||||
public ImmutableDictionary<string, FallbackSettings> FallbackCommands { get; init; }
|
||||
= ImmutableDictionary<string, FallbackSettings>.Empty;
|
||||
|
||||
public List<string> PinnedCommandIds { get; set; } = [];
|
||||
public ImmutableList<string> PinnedCommandIds { get; init; }
|
||||
= ImmutableList<string>.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public string ProviderDisplayName { get; set; } = string.Empty;
|
||||
public string ProviderId { get; init; } = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public string ProviderId { get; private set; } = string.Empty;
|
||||
public bool IsBuiltin { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsBuiltin { get; private set; }
|
||||
public string ProviderDisplayName { get; init; } = string.Empty;
|
||||
|
||||
public ProviderSettings(CommandProviderWrapper wrapper)
|
||||
public ProviderSettings()
|
||||
{
|
||||
Connect(wrapper);
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
@@ -41,28 +43,51 @@ public class ProviderSettings
|
||||
IsEnabled = isEnabled;
|
||||
}
|
||||
|
||||
public void Connect(CommandProviderWrapper wrapper)
|
||||
/// <summary>
|
||||
/// Returns a new ProviderSettings connected to the given wrapper.
|
||||
/// Returns <see langword="this"/> when the connection produces no changes.
|
||||
/// Pure function — does not mutate this instance.
|
||||
/// </summary>
|
||||
public ProviderSettings WithConnection(CommandProviderWrapper wrapper)
|
||||
{
|
||||
ProviderId = wrapper.ProviderId;
|
||||
IsBuiltin = wrapper.Extension is null;
|
||||
|
||||
ProviderDisplayName = wrapper.DisplayName;
|
||||
if (string.IsNullOrWhiteSpace(wrapper.ProviderId))
|
||||
{
|
||||
throw new ArgumentException("ProviderId must not be null, empty, or whitespace.", nameof(wrapper));
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
var builder = FallbackCommands.ToBuilder();
|
||||
if (wrapper.FallbackItems.Length > 0)
|
||||
{
|
||||
foreach (var fallback in wrapper.FallbackItems)
|
||||
{
|
||||
if (!FallbackCommands.ContainsKey(fallback.Id))
|
||||
if (!string.IsNullOrEmpty(fallback.Id) && !builder.ContainsKey(fallback.Id))
|
||||
{
|
||||
var enableGlobalResults = IsBuiltin && !_excludedBuiltInFallbacks.Contains(fallback.Id);
|
||||
FallbackCommands[fallback.Id] = new FallbackSettings(enableGlobalResults);
|
||||
var enableGlobalResults = (wrapper.Extension is null)
|
||||
&& !_excludedBuiltInFallbacks.Contains(fallback.Id);
|
||||
builder[fallback.Id] = new FallbackSettings(enableGlobalResults);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ProviderId))
|
||||
var isBuiltin = wrapper.Extension is null;
|
||||
|
||||
// If nothing changed, return the same instance to avoid unnecessary allocations and saves
|
||||
if (!changed
|
||||
&& ProviderId == wrapper.ProviderId
|
||||
&& IsBuiltin == isBuiltin
|
||||
&& ProviderDisplayName == wrapper.DisplayName)
|
||||
{
|
||||
throw new InvalidDataException("Did you add a built-in command and forget to set the Id? Make sure you do that!");
|
||||
return this;
|
||||
}
|
||||
|
||||
return this with
|
||||
{
|
||||
ProviderId = wrapper.ProviderId,
|
||||
IsBuiltin = isBuiltin,
|
||||
ProviderDisplayName = wrapper.DisplayName,
|
||||
FallbackCommands = changed ? builder.ToImmutable() : FallbackCommands,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -10,6 +10,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
@@ -21,20 +22,21 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
||||
private static readonly CompositeFormat ExtensionSubtextDisabledFormat = CompositeFormat.Parse(Resources.builtin_extension_subtext_disabled);
|
||||
|
||||
private readonly CommandProviderWrapper _provider;
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly Lock _initializeSettingsLock = new();
|
||||
|
||||
private ProviderSettings _providerSettings;
|
||||
|
||||
private Task? _initializeSettingsTask;
|
||||
|
||||
public ProviderSettingsViewModel(
|
||||
CommandProviderWrapper provider,
|
||||
ProviderSettings providerSettings,
|
||||
SettingsModel settings)
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_provider = provider;
|
||||
_providerSettings = providerSettings;
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
|
||||
LoadingSettings = _provider.Settings?.HasSettings ?? false;
|
||||
|
||||
@@ -70,8 +72,13 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
||||
{
|
||||
if (value != _providerSettings.IsEnabled)
|
||||
{
|
||||
_providerSettings.IsEnabled = value;
|
||||
Save();
|
||||
var newSettings = _providerSettings with { IsEnabled = value };
|
||||
_settingsService.UpdateSettings(s => s with
|
||||
{
|
||||
|
||||
ProviderSettings = s.ProviderSettings.SetItem(_provider.ProviderId, newSettings),
|
||||
});
|
||||
_providerSettings = newSettings;
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
OnPropertyChanged(nameof(IsEnabled));
|
||||
OnPropertyChanged(nameof(ExtensionSubtext));
|
||||
@@ -179,18 +186,31 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
||||
{
|
||||
if (_providerSettings.FallbackCommands.TryGetValue(fallbackItem.Id, out var fallbackSettings))
|
||||
{
|
||||
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, fallbackSettings, _settings, this));
|
||||
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, fallbackSettings, this, _settingsService));
|
||||
}
|
||||
else
|
||||
{
|
||||
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, new(), _settings, this));
|
||||
fallbackViewModels.Add(new FallbackSettingsViewModel(fallbackItem, new(), this, _settingsService));
|
||||
}
|
||||
}
|
||||
|
||||
FallbackCommands = fallbackViewModels;
|
||||
}
|
||||
|
||||
private void Save() => SettingsModel.SaveSettings(_settings);
|
||||
internal void UpdateFallbackSettings(string id, FallbackSettings settings)
|
||||
{
|
||||
var newProviderSettings = _providerSettings with
|
||||
{
|
||||
FallbackCommands = _providerSettings.FallbackCommands.SetItem(id, settings),
|
||||
};
|
||||
_providerSettings = newProviderSettings;
|
||||
_settingsService.UpdateSettings(
|
||||
s => s with
|
||||
{
|
||||
ProviderSettings = s.ProviderSettings.SetItem(_provider.ProviderId, newProviderSettings),
|
||||
},
|
||||
hotReload: false);
|
||||
}
|
||||
|
||||
private void InitializeSettingsPage()
|
||||
{
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class RecentCommandsManager : ObservableObject, IRecentCommandsManager
|
||||
public record RecentCommandsManager : IRecentCommandsManager
|
||||
{
|
||||
[JsonInclude]
|
||||
internal List<HistoryItem> History { get; set; } = [];
|
||||
|
||||
private readonly Lock _lock = new();
|
||||
internal ImmutableList<HistoryItem> History { get; init; } = ImmutableList<HistoryItem>.Empty;
|
||||
|
||||
public RecentCommandsManager()
|
||||
{
|
||||
@@ -20,64 +18,64 @@ public partial class RecentCommandsManager : ObservableObject, IRecentCommandsMa
|
||||
|
||||
public int GetCommandHistoryWeight(string commandId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var entry = History
|
||||
var entry = History
|
||||
.Index()
|
||||
.Where(item => item.Item.CommandId == commandId)
|
||||
.FirstOrDefault();
|
||||
|
||||
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
|
||||
// match after one use.
|
||||
// Usually it has a weight of 84, compared to 109 for the VS cmd prompt
|
||||
if (entry.Item is not null)
|
||||
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
|
||||
// match after one use.
|
||||
// Usually it has a weight of 84, compared to 109 for the VS cmd prompt
|
||||
if (entry.Item is not null)
|
||||
{
|
||||
var index = entry.Index;
|
||||
|
||||
// First, add some weight based on how early in the list this appears
|
||||
var bucket = index switch
|
||||
{
|
||||
var index = entry.Index;
|
||||
_ when index <= 2 => 35,
|
||||
_ when index <= 10 => 25,
|
||||
_ when index <= 15 => 15,
|
||||
_ when index <= 35 => 10,
|
||||
_ => 5,
|
||||
};
|
||||
|
||||
// First, add some weight based on how early in the list this appears
|
||||
var bucket = index switch
|
||||
{
|
||||
var i when index <= 2 => 35,
|
||||
var i when index <= 10 => 25,
|
||||
var i when index <= 15 => 15,
|
||||
var i when index <= 35 => 10,
|
||||
_ => 5,
|
||||
};
|
||||
// Then, add weight for how often this is used, but cap the weight from usage.
|
||||
var uses = Math.Min(entry.Item.Uses * 5, 35);
|
||||
|
||||
// Then, add weight for how often this is used, but cap the weight from usage.
|
||||
var uses = Math.Min(entry.Item.Uses * 5, 35);
|
||||
|
||||
return bucket + uses;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return bucket + uses;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void AddHistoryItem(string commandId)
|
||||
/// <summary>
|
||||
/// Returns a new RecentCommandsManager with the given command added/promoted in history.
|
||||
/// Pure function — does not mutate this instance.
|
||||
/// </summary>
|
||||
public RecentCommandsManager WithHistoryItem(string commandId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var entry = History
|
||||
.Where(item => item.CommandId == commandId)
|
||||
.FirstOrDefault();
|
||||
if (entry is null)
|
||||
{
|
||||
var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 };
|
||||
History.Insert(0, newitem);
|
||||
}
|
||||
else
|
||||
{
|
||||
History.Remove(entry);
|
||||
entry.Uses++;
|
||||
History.Insert(0, entry);
|
||||
}
|
||||
var existing = History.FirstOrDefault(item => item.CommandId == commandId);
|
||||
ImmutableList<HistoryItem> newHistory;
|
||||
|
||||
if (History.Count > 50)
|
||||
{
|
||||
History.RemoveRange(50, History.Count - 50);
|
||||
}
|
||||
if (existing is not null)
|
||||
{
|
||||
newHistory = History.Remove(existing);
|
||||
var updated = existing with { Uses = existing.Uses + 1 };
|
||||
newHistory = newHistory.Insert(0, updated);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newItem = new HistoryItem { CommandId = commandId, Uses = 1 };
|
||||
newHistory = History.Insert(0, newItem);
|
||||
}
|
||||
|
||||
if (newHistory.Count > 50)
|
||||
{
|
||||
newHistory = newHistory.RemoveRange(50, newHistory.Count - 50);
|
||||
}
|
||||
|
||||
return this with { History = newHistory };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,5 +83,5 @@ public interface IRecentCommandsManager
|
||||
{
|
||||
int GetCommandHistoryWeight(string commandId);
|
||||
|
||||
void AddHistoryItem(string commandId);
|
||||
RecentCommandsManager WithHistoryItem(string commandId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IAppStateService"/>.
|
||||
/// Handles loading, saving, and change notification for <see cref="AppStateModel"/>.
|
||||
/// </summary>
|
||||
public sealed class AppStateService : IAppStateService
|
||||
{
|
||||
private readonly IPersistenceService _persistence;
|
||||
private readonly IApplicationInfoService _appInfoService;
|
||||
private readonly string _filePath;
|
||||
|
||||
public AppStateService(IPersistenceService persistence, IApplicationInfoService appInfoService)
|
||||
{
|
||||
_persistence = persistence;
|
||||
_appInfoService = appInfoService;
|
||||
_filePath = StateJsonPath();
|
||||
_state = _persistence.Load(_filePath, JsonSerializationContext.Default.AppStateModel);
|
||||
}
|
||||
|
||||
private AppStateModel _state;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AppStateModel State => Volatile.Read(ref _state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event TypedEventHandler<IAppStateService, AppStateModel>? StateChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Save() => UpdateState(s => s);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateState(Func<AppStateModel, AppStateModel> transform)
|
||||
{
|
||||
AppStateModel snapshot;
|
||||
AppStateModel updated;
|
||||
do
|
||||
{
|
||||
snapshot = Volatile.Read(ref _state);
|
||||
updated = transform(snapshot);
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref _state, updated, snapshot) != snapshot);
|
||||
|
||||
var newState = Volatile.Read(ref _state);
|
||||
_persistence.Save(newState, _filePath, JsonSerializationContext.Default.AppStateModel);
|
||||
StateChanged?.Invoke(this, newState);
|
||||
}
|
||||
|
||||
private string StateJsonPath()
|
||||
{
|
||||
var directory = _appInfoService.ConfigDirectory;
|
||||
return Path.Combine(directory, "state.json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
internal sealed class ExtensionTemplateService : IExtensionTemplateService
|
||||
{
|
||||
internal enum TemplateFileHandling
|
||||
{
|
||||
ReplaceTokens,
|
||||
CopyAsIs,
|
||||
}
|
||||
|
||||
private const string TemplateArchiveRelativePath = "Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip";
|
||||
|
||||
private static readonly HashSet<string> _replaceTokensTemplateExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".appxmanifest",
|
||||
".config",
|
||||
".cs",
|
||||
".csproj",
|
||||
".json",
|
||||
".manifest",
|
||||
".props",
|
||||
".pubxml",
|
||||
".sln",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _copyAsIsTemplateExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".png",
|
||||
};
|
||||
|
||||
private readonly string _templateZipPath;
|
||||
|
||||
public ExtensionTemplateService()
|
||||
: this(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplateArchiveRelativePath))
|
||||
{
|
||||
}
|
||||
|
||||
internal ExtensionTemplateService(string templateZipPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(templateZipPath);
|
||||
_templateZipPath = templateZipPath;
|
||||
}
|
||||
|
||||
internal static IReadOnlyCollection<string> ReplaceTokensTemplateExtensions => _replaceTokensTemplateExtensions;
|
||||
|
||||
internal static IReadOnlyCollection<string> CopyAsIsTemplateExtensions => _copyAsIsTemplateExtensions;
|
||||
|
||||
public void CreateExtension(string extensionName, string displayName, string outputPath)
|
||||
{
|
||||
var newGuid = Guid.NewGuid().ToString();
|
||||
|
||||
// Unzip `template.zip` to a temp dir:
|
||||
var tempDir = Directory.CreateTempSubdirectory("CmdPal_ExtTemplate");
|
||||
|
||||
if (!Directory.Exists(outputPath))
|
||||
{
|
||||
Directory.CreateDirectory(outputPath);
|
||||
}
|
||||
|
||||
ZipFile.ExtractToDirectory(_templateZipPath, tempDir.FullName);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(tempDir.FullName, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
CopyTemplateFile(tempDir.FullName, file, outputPath, extensionName, displayName, newGuid);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
tempDir.Delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CopyTemplateFile(string templateRoot, string sourceFile, string outputPath, string extensionName, string displayName, string newGuid)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(templateRoot, sourceFile);
|
||||
var newFileName = Path.Combine(outputPath, relativePath).Replace("TemplateCmdPalExtension", extensionName, StringComparison.Ordinal);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFileName)!);
|
||||
|
||||
switch (GetTemplateFileHandling(sourceFile))
|
||||
{
|
||||
case TemplateFileHandling.ReplaceTokens:
|
||||
var sourceText = File.ReadAllText(sourceFile);
|
||||
var updatedText = ReplaceTemplateTokens(sourceText, extensionName, displayName, newGuid);
|
||||
if (string.Equals(sourceText, updatedText, StringComparison.Ordinal))
|
||||
{
|
||||
File.Copy(sourceFile, newFileName, overwrite: true);
|
||||
break;
|
||||
}
|
||||
|
||||
File.WriteAllText(newFileName, updatedText);
|
||||
break;
|
||||
case TemplateFileHandling.CopyAsIs:
|
||||
default:
|
||||
File.Copy(sourceFile, newFileName, overwrite: true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal static TemplateFileHandling GetTemplateFileHandling(string filePath)
|
||||
{
|
||||
var extension = Path.GetExtension(filePath);
|
||||
if (_replaceTokensTemplateExtensions.Contains(extension))
|
||||
{
|
||||
return TemplateFileHandling.ReplaceTokens;
|
||||
}
|
||||
|
||||
if (_copyAsIsTemplateExtensions.Contains(extension))
|
||||
{
|
||||
return TemplateFileHandling.CopyAsIs;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Template file '{filePath}' has unsupported extension '{extension}'. Update the template file handling lists in {nameof(ExtensionTemplateService)}.");
|
||||
}
|
||||
|
||||
private static string ReplaceTemplateTokens(string text, string extensionName, string displayName, string newGuid) =>
|
||||
text
|
||||
.Replace("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", newGuid, StringComparison.Ordinal)
|
||||
.Replace("TemplateCmdPalExtension", extensionName, StringComparison.Ordinal)
|
||||
.Replace("TemplateDisplayName", displayName, StringComparison.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the lifecycle of <see cref="AppStateModel"/>: load, save, and change notification.
|
||||
/// </summary>
|
||||
public interface IAppStateService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current application state instance.
|
||||
/// </summary>
|
||||
AppStateModel State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Persists the current state to disk and raises <see cref="StateChanged"/>.
|
||||
/// </summary>
|
||||
void Save();
|
||||
|
||||
/// <summary>
|
||||
/// Atomically applies a transformation to the current state, persists the result,
|
||||
/// and raises <see cref="StateChanged"/>.
|
||||
/// </summary>
|
||||
void UpdateState(Func<AppStateModel, AppStateModel> transform);
|
||||
|
||||
/// <summary>
|
||||
/// Raised after state has been saved to disk.
|
||||
/// </summary>
|
||||
event TypedEventHandler<IAppStateService, AppStateModel> StateChanged;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Creates new Command Palette extensions from the built-in project template.
|
||||
/// </summary>
|
||||
internal interface IExtensionTemplateService
|
||||
{
|
||||
/// <summary>
|
||||
/// Scaffolds a new extension project by extracting the template archive,
|
||||
/// replacing placeholder tokens, and writing the result to <paramref name="outputPath"/>.
|
||||
/// </summary>
|
||||
/// <param name="extensionName">The code-safe name used for the project and namespaces.</param>
|
||||
/// <param name="displayName">The human-readable name shown in the extension catalog.</param>
|
||||
/// <param name="outputPath">The directory where the new extension project will be created.</param>
|
||||
void CreateExtension(string extensionName, string displayName, string outputPath);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides AOT-compatible JSON file persistence with shallow-merge strategy.
|
||||
/// </summary>
|
||||
public interface IPersistenceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads and deserializes a model from the specified JSON file.
|
||||
/// Returns a new <typeparamref name="T"/> instance when the file is missing or unreadable.
|
||||
/// </summary>
|
||||
T Load<T>(string filePath, JsonTypeInfo<T> typeInfo)
|
||||
where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// Serializes <paramref name="model"/>, shallow-merges into the existing file
|
||||
/// (preserving unknown keys), and writes the result back to disk.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to persist.</param>
|
||||
/// <param name="filePath">Target JSON file path.</param>
|
||||
/// <param name="typeInfo">AOT-compatible type metadata.</param>
|
||||
void Save<T>(T model, string filePath, JsonTypeInfo<T> typeInfo);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the lifecycle of <see cref="SettingsModel"/>: load, save, migration, and change notification.
|
||||
/// </summary>
|
||||
public interface ISettingsService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current settings instance.
|
||||
/// </summary>
|
||||
SettingsModel Settings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Persists the current settings to disk.
|
||||
/// </summary>
|
||||
/// <param name="hotReload">When <see langword="true"/>, raises <see cref="SettingsChanged"/> after saving.</param>
|
||||
void Save(bool hotReload = true);
|
||||
|
||||
/// <summary>
|
||||
/// Atomically applies a transformation to the current settings, persists the result,
|
||||
/// and optionally raises <see cref="SettingsChanged"/>.
|
||||
/// </summary>
|
||||
void UpdateSettings(Func<SettingsModel, SettingsModel> transform, bool hotReload = true);
|
||||
|
||||
/// <summary>
|
||||
/// Raised after settings are saved with <paramref name="hotReload"/> enabled, or after <see cref="Reload"/>.
|
||||
/// </summary>
|
||||
event TypedEventHandler<ISettingsService, SettingsModel> SettingsChanged;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IPersistenceService"/> that reads/writes
|
||||
/// JSON files with a shallow-merge strategy to preserve unknown keys.
|
||||
/// </summary>
|
||||
public sealed class PersistenceService : IPersistenceService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public T Load<T>(string filePath, JsonTypeInfo<T> typeInfo)
|
||||
where T : new()
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Logger.LogDebug("Settings file not found at {FilePath}", filePath);
|
||||
return new T();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jsonContent = File.ReadAllText(filePath);
|
||||
var loaded = JsonSerializer.Deserialize(jsonContent, typeInfo);
|
||||
|
||||
if (loaded is null)
|
||||
{
|
||||
Logger.LogDebug("Failed to parse settings file at {FilePath}", filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Successfully loaded settings file from {FilePath}", filePath);
|
||||
}
|
||||
|
||||
return loaded ?? new T();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load settings from {filePath}:", ex);
|
||||
}
|
||||
|
||||
return new T();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Save<T>(T model, string filePath, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsJson = JsonSerializer.Serialize(model, typeInfo);
|
||||
|
||||
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
|
||||
{
|
||||
Logger.LogError("Failed to parse serialized model as JsonObject.");
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var serialized = newSettings.ToJsonString(typeInfo.Options);
|
||||
File.WriteAllText(filePath, serialized);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save to {filePath}:", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="ISettingsService"/>.
|
||||
/// Handles loading, saving, migration, and change notification for <see cref="SettingsModel"/>.
|
||||
/// </summary>
|
||||
public sealed class SettingsService : ISettingsService
|
||||
{
|
||||
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
|
||||
|
||||
private readonly IPersistenceService _persistence;
|
||||
private readonly IApplicationInfoService _appInfoService;
|
||||
private readonly string _filePath;
|
||||
|
||||
public SettingsService(IPersistenceService persistence, IApplicationInfoService appInfoService)
|
||||
{
|
||||
_persistence = persistence;
|
||||
_appInfoService = appInfoService;
|
||||
_filePath = SettingsJsonPath();
|
||||
_settings = _persistence.Load(_filePath, JsonSerializationContext.Default.SettingsModel);
|
||||
ApplyMigrations();
|
||||
}
|
||||
|
||||
private SettingsModel _settings;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SettingsModel Settings => Volatile.Read(ref _settings);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event TypedEventHandler<ISettingsService, SettingsModel>? SettingsChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Save(bool hotReload = true) => UpdateSettings(s => s, hotReload);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateSettings(Func<SettingsModel, SettingsModel> transform, bool hotReload = true)
|
||||
{
|
||||
SettingsModel snapshot;
|
||||
SettingsModel updated;
|
||||
do
|
||||
{
|
||||
snapshot = Volatile.Read(ref _settings);
|
||||
updated = transform(snapshot);
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref _settings, updated, snapshot) != snapshot);
|
||||
|
||||
var newSettings = Volatile.Read(ref _settings);
|
||||
_persistence.Save(newSettings, _filePath, JsonSerializationContext.Default.SettingsModel);
|
||||
if (hotReload)
|
||||
{
|
||||
SettingsChanged?.Invoke(this, newSettings);
|
||||
}
|
||||
}
|
||||
|
||||
private string SettingsJsonPath()
|
||||
{
|
||||
var directory = _appInfoService.ConfigDirectory;
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
private void ApplyMigrations()
|
||||
{
|
||||
var migratedAny = false;
|
||||
|
||||
try
|
||||
{
|
||||
var jsonContent = File.Exists(_filePath) ? File.ReadAllText(_filePath) : null;
|
||||
if (jsonContent is not null && JsonNode.Parse(jsonContent) is JsonObject root)
|
||||
{
|
||||
migratedAny |= TryMigrate(
|
||||
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
|
||||
root,
|
||||
ref _settings,
|
||||
nameof(SettingsModel.AutoGoHomeInterval),
|
||||
DeprecatedHotkeyGoesHomeKey,
|
||||
(ref SettingsModel model, bool goesHome) => model = model with { AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan },
|
||||
JsonSerializationContext.Default.Boolean);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Migration check failed: {ex}");
|
||||
}
|
||||
|
||||
if (migratedAny)
|
||||
{
|
||||
Save(hotReload: false);
|
||||
}
|
||||
}
|
||||
|
||||
private delegate void MigrationApply<T>(ref SettingsModel model, T value);
|
||||
|
||||
private static bool TryMigrate<T>(
|
||||
string migrationName,
|
||||
JsonObject root,
|
||||
ref SettingsModel model,
|
||||
string newKey,
|
||||
string oldKey,
|
||||
MigrationApply<T> apply,
|
||||
JsonTypeInfo<T> jsonTypeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (root.ContainsKey(newKey) && root[newKey] is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
|
||||
{
|
||||
var value = oldNode.Deserialize(jsonTypeInfo);
|
||||
apply(ref model, value!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error during migration {migrationName}.", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.UI;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
@@ -14,103 +14,95 @@ namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
|
||||
/// settings are in <see cref="DockBandSettings"/>.
|
||||
/// </summary>
|
||||
public class DockSettings
|
||||
public record DockSettings
|
||||
{
|
||||
public DockSide Side { get; set; } = DockSide.Top;
|
||||
public DockSide Side { get; init; } = DockSide.Top;
|
||||
|
||||
public DockSize DockSize { get; set; } = DockSize.Small;
|
||||
public DockSize DockSize { get; init; } = DockSize.Small;
|
||||
|
||||
public DockSize DockIconsSize { get; set; } = DockSize.Small;
|
||||
public DockSize DockIconsSize { get; init; } = DockSize.Small;
|
||||
|
||||
public bool AlwaysOnTop { get; set; } = true;
|
||||
|
||||
// <Theme settings>
|
||||
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
|
||||
public DockBackdrop Backdrop { get; init; } = DockBackdrop.Acrylic;
|
||||
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
public UserTheme Theme { get; init; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
public ColorizationMode ColorizationMode { get; init; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
public Color CustomThemeColor { get; init; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent and COM in class init
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
public int CustomThemeColorIntensity { get; init; } = 100;
|
||||
|
||||
public int BackgroundImageOpacity { get; set; } = 20;
|
||||
public int BackgroundImageOpacity { get; init; } = 20;
|
||||
|
||||
public int BackgroundImageBlurAmount { get; set; }
|
||||
public int BackgroundImageBlurAmount { get; init; }
|
||||
|
||||
public int BackgroundImageBrightness { get; set; }
|
||||
public int BackgroundImageBrightness { get; init; }
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
||||
public BackgroundImageFit BackgroundImageFit { get; init; }
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
public string? BackgroundImagePath { get; init; }
|
||||
|
||||
// </Theme settings>
|
||||
// public List<string> PinnedCommands { get; set; } = [];
|
||||
public List<DockBandSettings> StartBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> CenterBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> EndBands { get; set; } = [];
|
||||
|
||||
public bool ShowLabels { get; set; } = true;
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<(string ProviderId, string CommandId)> AllPinnedCommands =>
|
||||
StartBands.Select(b => (b.ProviderId, b.CommandId))
|
||||
.Concat(CenterBands.Select(b => (b.ProviderId, b.CommandId)))
|
||||
.Concat(EndBands.Select(b => (b.ProviderId, b.CommandId)));
|
||||
|
||||
public DockSettings()
|
||||
{
|
||||
// Initialize with default values
|
||||
// PinnedCommands = [
|
||||
// "com.microsoft.cmdpal.winget"
|
||||
// ];
|
||||
StartBands.Add(new DockBandSettings
|
||||
public ImmutableList<DockBandSettings> StartBands { get; init; } = ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "com.microsoft.cmdpal.builtin.core",
|
||||
CommandId = "com.microsoft.cmdpal.home",
|
||||
});
|
||||
StartBands.Add(new DockBandSettings
|
||||
},
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "WinGet",
|
||||
CommandId = "com.microsoft.cmdpal.winget",
|
||||
ShowLabels = false,
|
||||
});
|
||||
|
||||
EndBands.Add(new DockBandSettings
|
||||
public ImmutableList<DockBandSettings> CenterBands { get; init; } = ImmutableList<DockBandSettings>.Empty;
|
||||
|
||||
public ImmutableList<DockBandSettings> EndBands { get; init; } = ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "PerformanceMonitor",
|
||||
CommandId = "com.microsoft.cmdpal.performanceWidget",
|
||||
});
|
||||
EndBands.Add(new DockBandSettings
|
||||
},
|
||||
new DockBandSettings
|
||||
{
|
||||
ProviderId = "com.microsoft.cmdpal.builtin.datetime",
|
||||
CommandId = "com.microsoft.cmdpal.timedate.dockBand",
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShowLabels { get; init; } = true;
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<(string ProviderId, string CommandId)> AllPinnedCommands =>
|
||||
StartBands.Select(b => (b.ProviderId, b.CommandId))
|
||||
.Concat(CenterBands.Select(b => (b.ProviderId, b.CommandId)))
|
||||
.Concat(EndBands.Select(b => (b.ProviderId, b.CommandId)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settings for a specific dock band. These are per-band settings stored
|
||||
/// within the overall <see cref="DockSettings"/>.
|
||||
/// </summary>
|
||||
public class DockBandSettings
|
||||
public record DockBandSettings
|
||||
{
|
||||
public required string ProviderId { get; set; }
|
||||
public required string ProviderId { get; init; }
|
||||
|
||||
public required string CommandId { get; set; }
|
||||
public required string CommandId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether titles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowTitles { get; set; }
|
||||
public bool? ShowTitles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether subtitles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowSubtitles { get; set; }
|
||||
public bool? ShowSubtitles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
|
||||
@@ -119,7 +111,7 @@ public class DockBandSettings
|
||||
public bool? ShowLabels
|
||||
{
|
||||
get => ShowTitles;
|
||||
set => ShowTitles = value;
|
||||
init => ShowTitles = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,17 +127,6 @@ public class DockBandSettings
|
||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||
/// </summary>
|
||||
public bool ResolveShowSubtitles(bool defaultValue) => ShowSubtitles ?? defaultValue;
|
||||
|
||||
public DockBandSettings Clone()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ProviderId = this.ProviderId,
|
||||
CommandId = this.CommandId,
|
||||
ShowTitles = this.ShowTitles,
|
||||
ShowSubtitles = this.ShowSubtitles,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum DockSide
|
||||
|
||||
@@ -39,24 +39,24 @@ public record HotkeySettings// : ICmdLineRepresentable
|
||||
}
|
||||
|
||||
[JsonPropertyName("win")]
|
||||
public bool Win { get; set; }
|
||||
public bool Win { get; init; }
|
||||
|
||||
[JsonPropertyName("ctrl")]
|
||||
public bool Ctrl { get; set; }
|
||||
public bool Ctrl { get; init; }
|
||||
|
||||
[JsonPropertyName("alt")]
|
||||
public bool Alt { get; set; }
|
||||
public bool Alt { get; init; }
|
||||
|
||||
[JsonPropertyName("shift")]
|
||||
public bool Shift { get; set; }
|
||||
public bool Shift { get; init; }
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
public int Code { get; init; }
|
||||
|
||||
// This is currently needed for FancyZones, we need to unify these two objects
|
||||
// see src\common\settings_objects.h
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -2,126 +2,118 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.UI;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class SettingsModel : ObservableObject
|
||||
public record SettingsModel
|
||||
{
|
||||
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
|
||||
|
||||
[JsonIgnore]
|
||||
public static readonly string FilePath;
|
||||
|
||||
public event TypedEventHandler<SettingsModel, object?>? SettingsChanged;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// SETTINGS HERE
|
||||
public static HotkeySettings DefaultActivationShortcut { get; } = new HotkeySettings(true, false, true, false, 0x20); // win+alt+space
|
||||
|
||||
public HotkeySettings? Hotkey { get; set; } = DefaultActivationShortcut;
|
||||
public HotkeySettings? Hotkey { get; init; } = DefaultActivationShortcut;
|
||||
|
||||
public bool UseLowLevelGlobalHotkey { get; set; }
|
||||
public bool UseLowLevelGlobalHotkey { get; init; }
|
||||
|
||||
public bool ShowAppDetails { get; set; }
|
||||
public bool ShowAppDetails { get; init; }
|
||||
|
||||
public bool BackspaceGoesBack { get; set; }
|
||||
public bool BackspaceGoesBack { get; init; }
|
||||
|
||||
public bool SingleClickActivates { get; set; }
|
||||
public bool SingleClickActivates { get; init; }
|
||||
|
||||
public bool HighlightSearchOnActivate { get; set; } = true;
|
||||
public bool HighlightSearchOnActivate { get; init; } = true;
|
||||
|
||||
public bool KeepPreviousQuery { get; set; }
|
||||
public bool KeepPreviousQuery { get; init; }
|
||||
|
||||
public bool ShowSystemTrayIcon { get; set; } = true;
|
||||
public bool ShowSystemTrayIcon { get; init; } = true;
|
||||
|
||||
public bool IgnoreShortcutWhenFullscreen { get; set; }
|
||||
public bool IgnoreShortcutWhenFullscreen { get; init; } = true;
|
||||
|
||||
public bool AllowExternalReload { get; set; }
|
||||
public bool IgnoreShortcutWhenBusy { get; init; }
|
||||
|
||||
public Dictionary<string, ProviderSettings> ProviderSettings { get; set; } = [];
|
||||
public bool AllowBreakthroughShortcut { get; init; }
|
||||
|
||||
public string[] FallbackRanks { get; set; } = [];
|
||||
public bool AllowExternalReload { get; init; }
|
||||
|
||||
public Dictionary<string, CommandAlias> Aliases { get; set; } = [];
|
||||
public ImmutableDictionary<string, ProviderSettings> ProviderSettings { get; init; }
|
||||
= ImmutableDictionary<string, ProviderSettings>.Empty;
|
||||
|
||||
public List<TopLevelHotkey> CommandHotkeys { get; set; } = [];
|
||||
public string[] FallbackRanks { get; init; } = [];
|
||||
|
||||
public MonitorBehavior SummonOn { get; set; } = MonitorBehavior.ToMouse;
|
||||
public ImmutableDictionary<string, CommandAlias> Aliases { get; init; }
|
||||
= ImmutableDictionary<string, CommandAlias>.Empty;
|
||||
|
||||
public bool DisableAnimations { get; set; } = true;
|
||||
public ImmutableList<TopLevelHotkey> CommandHotkeys { get; init; }
|
||||
= ImmutableList<TopLevelHotkey>.Empty;
|
||||
|
||||
public WindowPosition? LastWindowPosition { get; set; }
|
||||
public MonitorBehavior SummonOn { get; init; } = MonitorBehavior.ToMouse;
|
||||
|
||||
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
|
||||
public bool DisableAnimations { get; init; } = true;
|
||||
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
public WindowPosition? LastWindowPosition { get; init; }
|
||||
|
||||
public bool EnableDock { get; set; }
|
||||
public TimeSpan AutoGoHomeInterval { get; init; } = Timeout.InfiniteTimeSpan;
|
||||
|
||||
public DockSettings DockSettings { get; set; } = new();
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; init; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
|
||||
public bool EnableDock { get; init; }
|
||||
|
||||
public DockSettings DockSettings { get; init; } = new();
|
||||
|
||||
// Theme settings
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
public UserTheme Theme { get; init; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
public ColorizationMode ColorizationMode { get; init; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
public Color CustomThemeColor { get; init; } = new() { A = 0, R = 255, G = 255, B = 255 }; // Transparent — avoids WinUI3 COM dependency on Colors.Transparent
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
public int CustomThemeColorIntensity { get; init; } = 100;
|
||||
|
||||
public int BackgroundImageTintIntensity { get; set; }
|
||||
public int BackgroundImageTintIntensity { get; init; }
|
||||
|
||||
public int BackgroundImageOpacity { get; set; } = 20;
|
||||
public int BackgroundImageOpacity { get; init; } = 20;
|
||||
|
||||
public int BackgroundImageBlurAmount { get; set; }
|
||||
public int BackgroundImageBlurAmount { get; init; }
|
||||
|
||||
public int BackgroundImageBrightness { get; set; }
|
||||
public int BackgroundImageBrightness { get; init; }
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
||||
public BackgroundImageFit BackgroundImageFit { get; init; }
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
public string? BackgroundImagePath { get; init; }
|
||||
|
||||
public BackdropStyle BackdropStyle { get; set; }
|
||||
public BackdropStyle BackdropStyle { get; init; }
|
||||
|
||||
public int BackdropOpacity { get; set; } = 100;
|
||||
public int BackdropOpacity { get; init; } = 100;
|
||||
|
||||
// </Theme settings>
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static SettingsModel()
|
||||
public (SettingsModel Model, ProviderSettings Settings) GetProviderSettings(CommandProviderWrapper provider)
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
}
|
||||
|
||||
public ProviderSettings GetProviderSettings(CommandProviderWrapper provider)
|
||||
{
|
||||
ProviderSettings? settings;
|
||||
if (!ProviderSettings.TryGetValue(provider.ProviderId, out settings))
|
||||
if (!ProviderSettings.TryGetValue(provider.ProviderId, out var settings))
|
||||
{
|
||||
settings = new ProviderSettings(provider);
|
||||
settings.Connect(provider);
|
||||
ProviderSettings[provider.ProviderId] = settings;
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Connect(provider);
|
||||
settings = new ProviderSettings();
|
||||
}
|
||||
|
||||
return settings;
|
||||
var connected = settings.WithConnection(provider);
|
||||
|
||||
// If WithConnection returned the same instance, nothing changed — skip SetItem
|
||||
if (ReferenceEquals(connected, settings))
|
||||
{
|
||||
return (this, connected);
|
||||
}
|
||||
|
||||
var newModel = this with
|
||||
{
|
||||
ProviderSettings = ProviderSettings.SetItem(provider.ProviderId, connected),
|
||||
};
|
||||
return (newModel, connected);
|
||||
}
|
||||
|
||||
public string[] GetGlobalFallbacks()
|
||||
@@ -143,165 +135,6 @@ public partial class SettingsModel : ObservableObject
|
||||
return globalFallbacks.ToArray();
|
||||
}
|
||||
|
||||
public static SettingsModel LoadSettings()
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(SettingsModel.FilePath)} before calling {nameof(LoadSettings)}");
|
||||
}
|
||||
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
Debug.WriteLine("The provided settings file does not exist");
|
||||
return new();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel) ?? new();
|
||||
|
||||
var migratedAny = false;
|
||||
try
|
||||
{
|
||||
if (JsonNode.Parse(jsonContent) is JsonObject root)
|
||||
{
|
||||
migratedAny |= ApplyMigrations(root, loaded);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Migration check failed: {ex}");
|
||||
}
|
||||
|
||||
Debug.WriteLine("Loaded settings file");
|
||||
|
||||
if (migratedAny)
|
||||
{
|
||||
SaveSettings(loaded);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.ToString());
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
private static bool ApplyMigrations(JsonObject root, SettingsModel model)
|
||||
{
|
||||
var migrated = false;
|
||||
|
||||
// Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)
|
||||
// The old 'HotkeyGoesHome' boolean indicated whether the "go home" action should happen immediately (true) or never (false).
|
||||
// The new 'AutoGoHomeInterval' uses a TimeSpan: 'TimeSpan.Zero' means immediate, 'Timeout.InfiniteTimeSpan' means never.
|
||||
migrated |= TryMigrate(
|
||||
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
|
||||
root,
|
||||
model,
|
||||
nameof(AutoGoHomeInterval),
|
||||
DeprecatedHotkeyGoesHomeKey,
|
||||
(settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
|
||||
JsonSerializationContext.Default.Boolean);
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
private static bool TryMigrate<T>(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action<SettingsModel, T> apply, JsonTypeInfo<T> jsonTypeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If new key already present, skip migration
|
||||
if (root.ContainsKey(newKey) && root[newKey] is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If old key present, try to deserialize and apply
|
||||
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
|
||||
{
|
||||
var value = oldNode.Deserialize<T>(jsonTypeInfo);
|
||||
apply(model, value!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error during migration {migrationName}.", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void SaveSettings(SettingsModel model, bool hotReload = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveSettings)}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Serialize the main dictionary to JSON and save it to the file
|
||||
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.SettingsModel);
|
||||
|
||||
// Is it valid JSON?
|
||||
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
|
||||
{
|
||||
// Now, read the existing content from the file
|
||||
var oldContent = File.Exists(FilePath) ? File.ReadAllText(FilePath) : "{}";
|
||||
|
||||
// Is it valid JSON?
|
||||
if (JsonNode.Parse(oldContent) is JsonObject savedSettings)
|
||||
{
|
||||
foreach (var item in newSettings)
|
||||
{
|
||||
savedSettings[item.Key] = item.Value?.DeepClone();
|
||||
}
|
||||
|
||||
// Remove deprecated keys
|
||||
savedSettings.Remove(DeprecatedHotkeyGoesHomeKey);
|
||||
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
// TODO: Instead of just raising the event here, we should
|
||||
// have a file change watcher on the settings file, and
|
||||
// reload the settings then
|
||||
if (hotReload)
|
||||
{
|
||||
model.SettingsChanged?.Invoke(model, null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed to parse settings file as JsonObject.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Failed to parse settings file as JsonObject.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the settings is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
|
||||
// private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||
// {
|
||||
@@ -329,6 +162,13 @@ public partial class SettingsModel : ObservableObject
|
||||
[JsonSerializable(typeof(RecentCommandsManager))]
|
||||
[JsonSerializable(typeof(List<string>), TypeInfoPropertyName = "StringList")]
|
||||
[JsonSerializable(typeof(List<HistoryItem>), TypeInfoPropertyName = "HistoryList")]
|
||||
[JsonSerializable(typeof(ImmutableList<HistoryItem>), TypeInfoPropertyName = "ImmutableHistoryList")]
|
||||
[JsonSerializable(typeof(ImmutableDictionary<string, FallbackSettings>), TypeInfoPropertyName = "ImmutableFallbackDictionary")]
|
||||
[JsonSerializable(typeof(ImmutableList<string>), TypeInfoPropertyName = "ImmutableStringList")]
|
||||
[JsonSerializable(typeof(ImmutableList<DockBandSettings>), TypeInfoPropertyName = "ImmutableDockBandSettingsList")]
|
||||
[JsonSerializable(typeof(ImmutableDictionary<string, ProviderSettings>), TypeInfoPropertyName = "ImmutableProviderSettingsDictionary")]
|
||||
[JsonSerializable(typeof(ImmutableDictionary<string, CommandAlias>), TypeInfoPropertyName = "ImmutableAliasDictionary")]
|
||||
[JsonSerializable(typeof(ImmutableList<TopLevelHotkey>), TypeInfoPropertyName = "ImmutableTopLevelHotkeyList")]
|
||||
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
|
||||
|
||||
@@ -27,7 +27,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
TimeSpan.FromSeconds(180),
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
@@ -38,123 +38,129 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
get => _settingsService.Settings.Hotkey;
|
||||
set
|
||||
{
|
||||
_settings.Hotkey = value ?? SettingsModel.DefaultActivationShortcut;
|
||||
_settingsService.UpdateSettings(s => s with { Hotkey = value ?? SettingsModel.DefaultActivationShortcut });
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseLowLevelGlobalHotkey
|
||||
{
|
||||
get => _settings.UseLowLevelGlobalHotkey;
|
||||
get => _settingsService.Settings.UseLowLevelGlobalHotkey;
|
||||
set
|
||||
{
|
||||
_settings.UseLowLevelGlobalHotkey = value;
|
||||
_settingsService.UpdateSettings(s => s with { UseLowLevelGlobalHotkey = value });
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AllowExternalReload
|
||||
{
|
||||
get => _settings.AllowExternalReload;
|
||||
get => _settingsService.Settings.AllowExternalReload;
|
||||
set
|
||||
{
|
||||
_settings.AllowExternalReload = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { AllowExternalReload = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAppDetails
|
||||
{
|
||||
get => _settings.ShowAppDetails;
|
||||
get => _settingsService.Settings.ShowAppDetails;
|
||||
set
|
||||
{
|
||||
_settings.ShowAppDetails = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { ShowAppDetails = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool BackspaceGoesBack
|
||||
{
|
||||
get => _settings.BackspaceGoesBack;
|
||||
get => _settingsService.Settings.BackspaceGoesBack;
|
||||
set
|
||||
{
|
||||
_settings.BackspaceGoesBack = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { BackspaceGoesBack = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool SingleClickActivates
|
||||
{
|
||||
get => _settings.SingleClickActivates;
|
||||
get => _settingsService.Settings.SingleClickActivates;
|
||||
set
|
||||
{
|
||||
_settings.SingleClickActivates = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { SingleClickActivates = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool HighlightSearchOnActivate
|
||||
{
|
||||
get => _settings.HighlightSearchOnActivate;
|
||||
get => _settingsService.Settings.HighlightSearchOnActivate;
|
||||
set
|
||||
{
|
||||
_settings.HighlightSearchOnActivate = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { HighlightSearchOnActivate = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool KeepPreviousQuery
|
||||
{
|
||||
get => _settings.KeepPreviousQuery;
|
||||
get => _settingsService.Settings.KeepPreviousQuery;
|
||||
set
|
||||
{
|
||||
_settings.KeepPreviousQuery = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { KeepPreviousQuery = value });
|
||||
}
|
||||
}
|
||||
|
||||
public int MonitorPositionIndex
|
||||
{
|
||||
get => (int)_settings.SummonOn;
|
||||
get => (int)_settingsService.Settings.SummonOn;
|
||||
set
|
||||
{
|
||||
_settings.SummonOn = (MonitorBehavior)value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { SummonOn = (MonitorBehavior)value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSystemTrayIcon
|
||||
{
|
||||
get => _settings.ShowSystemTrayIcon;
|
||||
get => _settingsService.Settings.ShowSystemTrayIcon;
|
||||
set
|
||||
{
|
||||
_settings.ShowSystemTrayIcon = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { ShowSystemTrayIcon = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnoreShortcutWhenFullscreen
|
||||
{
|
||||
get => _settings.IgnoreShortcutWhenFullscreen;
|
||||
get => _settingsService.Settings.IgnoreShortcutWhenFullscreen;
|
||||
set
|
||||
{
|
||||
_settings.IgnoreShortcutWhenFullscreen = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { IgnoreShortcutWhenFullscreen = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnoreShortcutWhenBusy
|
||||
{
|
||||
get => _settingsService.Settings.IgnoreShortcutWhenBusy;
|
||||
set
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { IgnoreShortcutWhenBusy = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool AllowBreakthroughShortcut
|
||||
{
|
||||
get => _settingsService.Settings.AllowBreakthroughShortcut;
|
||||
set
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { AllowBreakthroughShortcut = value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool DisableAnimations
|
||||
{
|
||||
get => _settings.DisableAnimations;
|
||||
get => _settingsService.Settings.DisableAnimations;
|
||||
set
|
||||
{
|
||||
_settings.DisableAnimations = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { DisableAnimations = value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +168,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval);
|
||||
var index = AutoGoHomeIntervals.IndexOf(_settingsService.Settings.AutoGoHomeInterval);
|
||||
return index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
@@ -170,70 +176,71 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
if (value >= 0 && value < AutoGoHomeIntervals.Count)
|
||||
{
|
||||
_settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
|
||||
_settingsService.UpdateSettings(s => s with { AutoGoHomeInterval = AutoGoHomeIntervals[value] });
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public int EscapeKeyBehaviorIndex
|
||||
{
|
||||
get => (int)_settings.EscapeKeyBehaviorSetting;
|
||||
get => (int)_settingsService.Settings.EscapeKeyBehaviorSetting;
|
||||
set
|
||||
{
|
||||
_settings.EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value });
|
||||
}
|
||||
}
|
||||
|
||||
public DockSide Dock_Side
|
||||
{
|
||||
get => _settings.DockSettings.Side;
|
||||
get => _settingsService.Settings.DockSettings.Side;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Side = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { Side = value } });
|
||||
}
|
||||
}
|
||||
|
||||
public DockSize Dock_DockSize
|
||||
{
|
||||
get => _settings.DockSettings.DockSize;
|
||||
get => _settingsService.Settings.DockSettings.DockSize;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.DockSize = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { DockSize = value } });
|
||||
}
|
||||
}
|
||||
|
||||
public DockBackdrop Dock_Backdrop
|
||||
{
|
||||
get => _settings.DockSettings.Backdrop;
|
||||
get => _settingsService.Settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Backdrop = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { Backdrop = value } });
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dock_ShowLabels
|
||||
{
|
||||
get => _settings.DockSettings.ShowLabels;
|
||||
get => _settingsService.Settings.DockSettings.ShowLabels;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.ShowLabels = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { ShowLabels = value } });
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dock_AlwaysOnTop
|
||||
{
|
||||
get => _settingsService.Settings.DockSettings.AlwaysOnTop;
|
||||
set
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { AlwaysOnTop = value } });
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableDock
|
||||
{
|
||||
get => _settings.EnableDock;
|
||||
get => _settingsService.Settings.EnableDock;
|
||||
set
|
||||
{
|
||||
_settings.EnableDock = value;
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { EnableDock = value });
|
||||
WeakReferenceMessenger.Default.Send(new ShowHideDockMessage(value));
|
||||
WeakReferenceMessenger.Default.Send(new ReloadCommandsMessage()); // TODO! we need to update the MoreCommands of all top level items, but we don't _really_ want to reload
|
||||
}
|
||||
@@ -245,29 +252,45 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public SettingsExtensionsViewModel Extensions { get; }
|
||||
|
||||
public SettingsViewModel(SettingsModel settings, TopLevelCommandManager topLevelCommandManager, TaskScheduler scheduler, IThemeService themeService)
|
||||
public SettingsViewModel(
|
||||
TopLevelCommandManager topLevelCommandManager,
|
||||
TaskScheduler scheduler,
|
||||
IThemeService themeService,
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
|
||||
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, settingsService);
|
||||
DockAppearance = new DockAppearanceSettingsViewModel(themeService, settingsService);
|
||||
|
||||
var activeProviders = GetCommandProviders();
|
||||
var allProviderSettings = _settings.ProviderSettings;
|
||||
var allProviderSettings = _settingsService.Settings.ProviderSettings;
|
||||
|
||||
var fallbacks = new List<FallbackSettingsViewModel>();
|
||||
var currentRankings = _settings.FallbackRanks;
|
||||
var currentRankings = _settingsService.Settings.FallbackRanks;
|
||||
var needsSave = false;
|
||||
var currentSettingsModel = _settingsService.Settings;
|
||||
|
||||
foreach (var item in activeProviders)
|
||||
{
|
||||
var providerSettings = settings.GetProviderSettings(item);
|
||||
var (newModel, providerSettings) = currentSettingsModel.GetProviderSettings(item);
|
||||
currentSettingsModel = newModel;
|
||||
|
||||
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _settings);
|
||||
CommandProviders.Add(settingsModel);
|
||||
var providerSettingsModel = new ProviderSettingsViewModel(item, providerSettings, settingsService);
|
||||
CommandProviders.Add(providerSettingsModel);
|
||||
|
||||
fallbacks.AddRange(settingsModel.FallbackCommands);
|
||||
fallbacks.AddRange(providerSettingsModel.FallbackCommands);
|
||||
}
|
||||
|
||||
// Only persist if provider enumeration actually changed the model
|
||||
// Smelly? Yes, but it avoids an unnecessary write to disk.
|
||||
// I don't love it, but it seems better than the alternatives.
|
||||
// Open to suggestions.
|
||||
if (!ReferenceEquals(currentSettingsModel, _settingsService.Settings))
|
||||
{
|
||||
var finalModel = currentSettingsModel;
|
||||
_settingsService.UpdateSettings(_ => finalModel, hotReload: false);
|
||||
}
|
||||
|
||||
var fallbackRankings = new List<Scored<FallbackSettingsViewModel>>(fallbacks.Count);
|
||||
@@ -306,10 +329,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public void ApplyFallbackSort()
|
||||
{
|
||||
_settings.FallbackRanks = FallbackRankings.Select(s => s.Id).ToArray();
|
||||
Save();
|
||||
_settingsService.UpdateSettings(s => s with { FallbackRanks = FallbackRankings.Select(s2 => s2.Id).ToArray() });
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackRankings)));
|
||||
}
|
||||
|
||||
private void Save() => SettingsModel.SaveSettings(_settings);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user