mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-30 08:56:33 +01:00
Compare commits
58 Commits
stable-bac
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b568e93dc | ||
|
|
4b1e0f6220 | ||
|
|
1783812f1f | ||
|
|
94b5bc62de | ||
|
|
0d18727e81 | ||
|
|
1e40d6b15b | ||
|
|
de00cbf20a | ||
|
|
c4e96c7ee9 | ||
|
|
103429b4d7 | ||
|
|
01fb831e4e | ||
|
|
d197af3da9 | ||
|
|
a4791cc493 | ||
|
|
1a1894472a | ||
|
|
5d6f96559c | ||
|
|
b774e13176 | ||
|
|
e256e79685 | ||
|
|
623c804093 | ||
|
|
20188bda9b | ||
|
|
6e5ad11bc3 | ||
|
|
0e36e7e7a7 | ||
|
|
82dc4cdc18 | ||
|
|
c71fdca277 | ||
|
|
a69f7fa806 | ||
|
|
cd5f753140 | ||
|
|
c628b4901d | ||
|
|
c6c7bfb861 | ||
|
|
d64f06906c | ||
|
|
c26dfef81b | ||
|
|
dd420509ab | ||
|
|
2c4aab9d87 | ||
|
|
52ce33d438 | ||
|
|
fc1307418e | ||
|
|
f45d54abdf | ||
|
|
f28d009131 | ||
|
|
bc8adb3189 | ||
|
|
b67d3b4418 | ||
|
|
ae6da3235b | ||
|
|
b1985bc8d1 | ||
|
|
b3628aa068 | ||
|
|
55e974dea4 | ||
|
|
92d9f1061c | ||
|
|
63da56fae0 | ||
|
|
86fed8aa70 | ||
|
|
446d1baa6f | ||
|
|
f86d988fb1 | ||
|
|
cea78c31f8 | ||
|
|
f4e9af9acb | ||
|
|
50113aa50d | ||
|
|
7830466b81 | ||
|
|
cca66eacd0 | ||
|
|
7b9d5af8c1 | ||
|
|
471022e842 | ||
|
|
bb6f9a8b08 | ||
|
|
05b605ef27 | ||
|
|
7c8b30246e | ||
|
|
4d47659ff9 | ||
|
|
97e62b3253 | ||
|
|
cd5b76c988 |
20
.github/actions/spell-check/expect.txt
vendored
20
.github/actions/spell-check/expect.txt
vendored
@@ -94,6 +94,7 @@ ASSOCSTR
|
||||
ASYNCWINDOWPLACEMENT
|
||||
ASYNCWINDOWPOS
|
||||
atl
|
||||
ATX
|
||||
ATRIOX
|
||||
aumid
|
||||
Authenticode
|
||||
@@ -113,6 +114,7 @@ azman
|
||||
bbwe
|
||||
BCIE
|
||||
bck
|
||||
backticks
|
||||
BESTEFFORT
|
||||
bezelled
|
||||
bhid
|
||||
@@ -193,6 +195,7 @@ changecursor
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
cim
|
||||
@@ -267,6 +270,7 @@ countof
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
cppcoreguidelines
|
||||
cplusplus
|
||||
CPower
|
||||
cpptools
|
||||
@@ -369,7 +373,7 @@ devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
devpal
|
||||
DFX
|
||||
dfx
|
||||
DIALOGEX
|
||||
digicert
|
||||
DINORMAL
|
||||
@@ -383,6 +387,7 @@ DISPLAYFREQUENCY
|
||||
displayname
|
||||
DISPLAYORIENTATION
|
||||
divyan
|
||||
djwsxzxb
|
||||
Dlg
|
||||
DLGFRAME
|
||||
DLGMODALFRAME
|
||||
@@ -443,6 +448,7 @@ EDITSHORTCUTS
|
||||
EDITTEXT
|
||||
EFile
|
||||
ekus
|
||||
eku
|
||||
emojis
|
||||
ENABLEDELAYEDEXPANSION
|
||||
ENABLEDPOPUP
|
||||
@@ -507,8 +513,10 @@ eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fdx
|
||||
fesf
|
||||
FFFF
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
fileexplorerpreview
|
||||
@@ -635,6 +643,7 @@ Hiber
|
||||
Hiberboot
|
||||
HIBYTE
|
||||
hicon
|
||||
HICONSM
|
||||
HIDEREADONLY
|
||||
HIDEWINDOW
|
||||
Hif
|
||||
@@ -742,7 +751,7 @@ INITDIALOG
|
||||
INITGUID
|
||||
INITTOLOGFONTSTRUCT
|
||||
INLINEPREFIX
|
||||
Inlines
|
||||
inlines
|
||||
INPC
|
||||
inproc
|
||||
INPUTHARDWARE
|
||||
@@ -815,6 +824,7 @@ keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
ksa
|
||||
kvp
|
||||
Kybd
|
||||
LARGEICON
|
||||
@@ -921,6 +931,7 @@ LWA
|
||||
lwin
|
||||
LZero
|
||||
MAGTRANSFORM
|
||||
makeappx
|
||||
MAKEINTRESOURCE
|
||||
MAKEINTRESOURCEA
|
||||
MAKEINTRESOURCEW
|
||||
@@ -1148,6 +1159,7 @@ ntfs
|
||||
NTSTATUS
|
||||
NTSYSAPI
|
||||
NULLCURSOR
|
||||
nullref
|
||||
nullonfailure
|
||||
numberbox
|
||||
nwc
|
||||
@@ -1252,6 +1264,7 @@ pinvoke
|
||||
pipename
|
||||
PKBDLLHOOKSTRUCT
|
||||
pkgfamily
|
||||
PKI
|
||||
plib
|
||||
ploc
|
||||
ploca
|
||||
@@ -1693,6 +1706,7 @@ syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
SYSMENU
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
SYSTEMTIME
|
||||
@@ -1979,7 +1993,7 @@ WORKSPACESEDITOR
|
||||
WORKSPACESLAUNCHER
|
||||
WORKSPACESSNAPSHOTTOOL
|
||||
WORKSPACESWINDOWARRANGER
|
||||
Worktree
|
||||
worktree
|
||||
wox
|
||||
wparam
|
||||
wpf
|
||||
|
||||
64
.github/copilot-instructions.md
vendored
64
.github/copilot-instructions.md
vendored
@@ -1,43 +1,59 @@
|
||||
# PowerToys – Copilot guide (concise)
|
||||
---
|
||||
description: PowerToys AI contributor guidance.
|
||||
applyTo: pullRequests
|
||||
---
|
||||
|
||||
# PowerToys - Copilot guide (concise)
|
||||
|
||||
This is the top-level guide for AI changes. Keep edits small, follow existing patterns, and cite exact paths in PRs.
|
||||
|
||||
Repo map (1‑line per area)
|
||||
# Repo map (1-line per area)
|
||||
- Core apps: `src/runner/**` (tray/loader), `src/settings-ui/**` (Settings app)
|
||||
- Shared libs: `src/common/**`
|
||||
- Modules: `src/modules/*` (one per utility; Command Palette in `src/modules/cmdpal/**`)
|
||||
- Build tools/docs: `tools/**`, `doc/devdocs/**`
|
||||
|
||||
Build and test (defaults)
|
||||
# Build and test (defaults)
|
||||
- Prerequisites: Visual Studio 2022 17.4+, minimal Windows 10 1803+.
|
||||
- Build discipline:
|
||||
- One terminal per operation (build → test). Don’t switch/open new ones mid-flow.
|
||||
- One terminal per operation (build -> test). Do not switch or open new ones mid-flow.
|
||||
- After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`).
|
||||
- Use script(s) to build, synchronously block and wait in foreground for it to finish: `tools/build/build.ps1|.cmd` (current folder), `build-essentials.*` (once per brand new build for missing nuget packages)
|
||||
- Treat build **exit code 0** as success; any non-zero exit code is a failure, have Copilot read the errors log in the build folder (e.g., `build.*.*.errors.log`) and surface problems.
|
||||
- Don’t start tests or launch Runner until the previous step succeeded.
|
||||
- Tests (fast + targeted):
|
||||
- Find the test project by product code prefix (e.g., FancyZones, AdvancedPaste). Look for a sibling folder or 1–2 levels up named like `<Product>*UnitTests` or `<Product>*UITests`.
|
||||
- Build the test project, wait for **exit**, then run only those tests via VS Test Explorer or `vstest.console.exe` with filters. Avoid `dotnet test` in this repo.
|
||||
- Add/adjust tests when changing behavior; if skipped, state why (e.g., comment-only, string rename).
|
||||
- Use scripts to build, synchronously block and wait in foreground for completion: `tools/build/build.ps1|.cmd` (current folder), `build-essentials.*` (once per brand new build for missing nuget packages).
|
||||
- Treat build exit code 0 as success; any non-zero exit code is a failure. Read the errors log in the build folder (such as `build.*.*.errors.log`) and surface problems.
|
||||
- Do not start tests or launch Runner until the previous step succeeded.
|
||||
- Tests (fast and targeted):
|
||||
- Find the test project by product code prefix (for example FancyZones, AdvancedPaste). Look for a sibling folder or one to two levels up named like `<Product>*UnitTests` or `<Product>*UITests`.
|
||||
- Build the test project, wait for exit, then run only those tests via VS Test Explorer or `vstest.console.exe` with filters. Avoid `dotnet test` in this repo.
|
||||
- Add or adjust tests when changing behavior; if skipped, state why (for example comment-only or string rename).
|
||||
|
||||
Pull requests (expectations)
|
||||
- Atomic: one logical change; no drive‑by refactors.
|
||||
- Describe: problem / approach / risk / test evidence.
|
||||
# Pull requests (expectations)
|
||||
- Atomic: one logical change; no drive-by refactors.
|
||||
- Describe: problem, approach, risk, test evidence.
|
||||
- List: touched paths if not obvious.
|
||||
|
||||
When to ask for clarification
|
||||
# When to ask for clarification
|
||||
- Ambiguous spec after scanning relevant docs (see below).
|
||||
- Cross-module impact (shared enum/struct) not clear.
|
||||
- Security / elevation / installer changes.
|
||||
- Cross-module impact (shared enum or struct) not clear.
|
||||
- Security, elevation, or installer changes.
|
||||
|
||||
Logging (use existing stacks)
|
||||
- C++: `src/common/logger/**` (`Logger::info|warn|error|debug`). Keep hot paths quiet (hooks, tight loops).
|
||||
- C#: `ManagedCommon.Logger` (`LogInfo|LogWarning|LogError|LogDebug|LogTrace`). Some UIs use injected `ILogger` via `LoggerInstance.Logger`.
|
||||
# Logging (use existing stacks)
|
||||
- C++ logging lives in `src/common/logger/**` (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`). Keep hot paths quiet (hooks, tight loops).
|
||||
- C# logging goes through `ManagedCommon.Logger` (`LogInfo`, `LogWarning`, `LogError`, `LogDebug`, `LogTrace`). Some UIs use injected `ILogger` via `LoggerInstance.Logger`.
|
||||
|
||||
Docs to consult
|
||||
# Docs to consult
|
||||
- `tools/build/BUILD-GUIDELINES.md`
|
||||
- `doc/devdocs/core/architecture.md`, `doc/devdocs/core/runner.md`, `doc/devdocs/core/settings/readme.md`, `doc/devdocs/modules/readme.md`
|
||||
- `doc/devdocs/core/architecture.md`
|
||||
- `doc/devdocs/core/runner.md`
|
||||
- `doc/devdocs/core/settings/readme.md`
|
||||
- `doc/devdocs/modules/readme.md`
|
||||
|
||||
Done checklist (self review before finishing)
|
||||
- Build clean? Tests updated/passed? No unintended formatting? Any new dependency? Documented skips?
|
||||
# Language style rules
|
||||
- Always enforce repo analyzers: root `.editorconfig` plus any `stylecop.json`.
|
||||
- C# code follows StyleCop.Analyzers and Microsoft.CodeAnalysis.NetAnalyzers.
|
||||
- C++ code honors `.clang-format` plus `.clang-tidy` (modernize/cppcoreguidelines/readability).
|
||||
- Markdown files wrap at 80 characters and use ATX headers with fenced code blocks that include language tags.
|
||||
- YAML files indent two spaces and add comments for complex settings while keeping keys clear.
|
||||
- PowerShell scripts use Verb-Noun names and prefer single-quoted literals while documenting parameters and satisfying PSScriptAnalyzer.
|
||||
|
||||
# Done checklist (self review before finishing)
|
||||
- Build clean? Tests updated or passed? No unintended formatting? Any new dependency? Documented skips?
|
||||
|
||||
16
.github/prompts/create-commit-title.prompt.md
vendored
Normal file
16
.github/prompts/create-commit-title.prompt.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Generate an 80-character git commit title for the local diff.'
|
||||
---
|
||||
|
||||
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
|
||||
|
||||
**Workflow:**
|
||||
1. Run a single command to view the local diff since the last commit:
|
||||
```@terminal
|
||||
git diff HEAD
|
||||
```
|
||||
2. From that diff, identify the dominant area (reference key paths like `src/modules/*`, `doc/devdocs/**`, etc.), the type of change (bug fix, docs update, config tweak), and any notable impact.
|
||||
3. Draft a concise, imperative commit title summarizing the dominant change. Keep it plain ASCII, <= 80 characters, and avoid trailing punctuation. Mention the primary component when obvious (for example `FancyZones:` or `Docs:`).
|
||||
4. Respond with only the final commit title on a single line so it can be pasted directly into `git commit`.
|
||||
22
.github/prompts/create-pr-summary.prompt.md
vendored
Normal file
22
.github/prompts/create-pr-summary.prompt.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff.'
|
||||
---
|
||||
|
||||
**Goal:** Produce a ready-to-paste PR title and description that follows PowerToys conventions by comparing the current branch against a user-selected target branch.
|
||||
|
||||
**Repo guardrails:**
|
||||
- Treat `.github/pull_request_template.md` as the single source of truth; load it at runtime instead of embedding hardcoded content in this prompt.
|
||||
- Preserve section order from the template but only surface checklist lines that are relevant for the detected changes, filling them with `[x]`/`[ ]` as appropriate.
|
||||
- Cite touched paths with inline backticks, matching the guidance in `.github/copilot-instructions.md`.
|
||||
- Call out test coverage explicitly: list automated tests run (unit/UI) or state why they are not applicable.
|
||||
|
||||
**Workflow:**
|
||||
1. Determine the target branch from user context; default to `main` when no branch is supplied.
|
||||
2. Run `git status --short` once to surface uncommitted files that may influence the summary.
|
||||
3. Run `git diff <target-branch>...HEAD` a single time to review the detailed changes. Only when confidence stays low dig deeper with focused calls such as `git diff <target-branch>...HEAD -- <path>`.
|
||||
4. From the diff, capture impacted areas, key file changes, behavioral risks, migrations, and noteworthy edge cases.
|
||||
5. Confirm validation: list tests executed with results or state why tests were skipped in line with repo guidance.
|
||||
6. Load `.github/pull_request_template.md`, mirror its section order, and populate it with the gathered facts. Include only relevant checklist entries, marking them `[x]/[ ]` and noting any intentional omissions as "N/A".
|
||||
7. Present the filled template inside a fenced ```markdown code block with no extra commentary so it is ready to paste into a PR, clearly flagging any placeholders that still need user input.
|
||||
22
.github/prompts/fix-spelling.prompt.md
vendored
Normal file
22
.github/prompts/fix-spelling.prompt.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR.'
|
||||
---
|
||||
|
||||
**Goal:** Clear every outstanding GitHub pull request comment created by the `Code scanning / check-spelling` workflow by explicitly allowing intentional terms.
|
||||
|
||||
**Guardrails:**
|
||||
- Update only discussion threads authored by `github-actions` or `github-actions[bot]` that mention `Code scanning results / check-spelling`.
|
||||
- Resolve findings solely by editing `.github/actions/spell-check/expect.txt`; reuse existing entries.
|
||||
- Leave all other files and topics untouched.
|
||||
|
||||
**Prerequisites:**
|
||||
- Install GitHub CLI if it is not present: `winget install GitHub.cli`.
|
||||
- Run `gh auth login` once before the first CLI use.
|
||||
|
||||
**Workflow:**
|
||||
1. Determine the active pull request with a single `gh pr view --json number` call (default to the current branch).
|
||||
2. Fetch all PR discussion data once via `gh pr view --json comments,reviews` and filter to check-spelling comments authored by `github-actions` or `github-actions[bot]` that are not minimized; when several remain, process only the most recent comment body.
|
||||
3. For each flagged token, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
|
||||
4. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.
|
||||
@@ -235,7 +235,9 @@
|
||||
"*Microsoft.CmdPal.UI_*.msix",
|
||||
|
||||
"PowerToys.DSC.dll",
|
||||
"PowerToys.DSC.exe"
|
||||
"PowerToys.DSC.exe",
|
||||
|
||||
"PowerToysSparse.msix"
|
||||
],
|
||||
"SigningInfo": {
|
||||
"Operations": [
|
||||
|
||||
@@ -1,6 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!-- Hybrid CRT configuration -->
|
||||
<PropertyGroup Condition="'$(HybridCrtConfiguration)'==''">
|
||||
<HybridCrtConfiguration>$(Configuration)</HybridCrtConfiguration>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Skip Hybrid CRT for AppContainer/UWP projects as they require MultiThreadedDLL -->
|
||||
<PropertyGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop'">
|
||||
<HybridCrtConfiguration></HybridCrtConfiguration>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemDefinitionGroup Condition="'$(HybridCrtConfiguration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(HybridCrtConfiguration)'=='Release'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- AppContainer/UWP projects must use MultiThreadedDLL -->
|
||||
<ItemDefinitionGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop' And '$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop' And '$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- Project configurations -->
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@@ -73,7 +123,6 @@
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -83,7 +132,6 @@
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4948" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
|
||||
@@ -26,6 +26,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runner", "src\runner\runner
|
||||
{D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D} = {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}
|
||||
{D940E07F-532C-4FF3-883F-790DA014F19A} = {D940E07F-532C-4FF3-883F-790DA014F19A}
|
||||
{DA425894-6E13-404F-8DCB-78584EC0557A} = {DA425894-6E13-404F-8DCB-78584EC0557A}
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D} = {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}
|
||||
{E364F67B-BB12-4E91-B639-355866EBCD8B} = {E364F67B-BB12-4E91-B639-355866EBCD8B}
|
||||
{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}
|
||||
EndProjectSection
|
||||
@@ -50,6 +51,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{1AFB64
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Common.Lib.UnitTests", "src\common\UnitTests-CommonLib\UnitTests-CommonLib.vcxproj", "{1A066C63-64B3-45F8-92FE-664E1CCE8077}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PackageIdentity", "src\PackageIdentity\PackageIdentity.vcxproj", "{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditor", "src\modules\fancyzones\editor\FancyZonesEditor\FancyZonesEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "powerrename", "powerrename", "{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}"
|
||||
@@ -71,6 +74,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.UnitTests", "sr
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ModuleTemplateCompileTest", "tools\project_template\ModuleTemplate\ModuleTemplateCompileTest.vcxproj", "{64A80062-4D8B-4229-8A38-DFA1D7497749}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ptcli", "tools\ptcli\ptcli.csproj", "{2589570C-B068-41CA-A554-BDCAE6FC4CAC}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManager", "src\modules\keyboardmanager\dll\KeyboardManager.vcxproj", "{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "imageresizer", "imageresizer", "{6C7F47CC-2151-44A3-A546-41C70025132C}"
|
||||
@@ -867,6 +872,14 @@ Global
|
||||
{1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.ActiveCfg = Release|x64
|
||||
{1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.Build.0 = Release|x64
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|x64.Build.0 = Debug|x64
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|x64.ActiveCfg = Release|x64
|
||||
{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|x64.Build.0 = Release|x64
|
||||
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -915,6 +928,14 @@ Global
|
||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.ActiveCfg = Release|x64
|
||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.Build.0 = Release|x64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|x64.Build.0 = Debug|x64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|x64.ActiveCfg = Release|x64
|
||||
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|x64.Build.0 = Release|x64
|
||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x64.ActiveCfg = Debug|x64
|
||||
|
||||
313
README.md
313
README.md
@@ -10,11 +10,11 @@
|
||||
|
||||
<h3 align="center">
|
||||
<a href="#-installation">Installation</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="https://aka.ms/powertoys-docs">Documentation</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="https://aka.ms/powertoys-releaseblog">Blog</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="#-whats-new">Release notes</a>
|
||||
</h3>
|
||||
<br/><br/>
|
||||
@@ -27,11 +27,12 @@ Microsoft PowerToys is a collection of utilities that help you customize Windows
|
||||
| [<img src="doc/images/icons/Color%20Picker.png" alt="Color Picker icon" height="16"> Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [<img src="doc/images/icons/Command%20Not%20Found.png" alt="Command Not Found icon" height="16"> Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [<img src="doc/images/icons/Command Palette.png" alt="Command Palette icon" height="16"> Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
|
||||
| [<img src="doc/images/icons/Crop%20And%20Lock.png" alt="Crop and Lock icon" height="16"> Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [<img src="doc/images/icons/Environment%20Manager.png" alt="Environment Variables icon" height="16"> Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [<img src="doc/images/icons/FancyZones.png" alt="FancyZones icon" height="16"> FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
|
||||
| [<img src="doc/images/icons/File%20Explorer%20Preview.png" alt="File Explorer Add-ons icon" height="16"> File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [<img src="doc/images/icons/File%20Locksmith.png" alt="File Locksmith icon" height="16"> File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [<img src="doc/images/icons/Host%20File%20Editor.png" alt="Hosts File Editor icon" height="16"> Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
|
||||
| [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
|
||||
| [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) | [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) |
|
||||
| [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
|
||||
| [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
|
||||
| [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
|
||||
| [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [<img src="doc/images/icons/Light Switch.png" alt="Light Switch icon" height="16"> Light Switch](https://aka.ms/PowerToysOverview_LightSwitch) |
|
||||
| [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) |
|
||||
| [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) | [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
|
||||
| [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
|
||||
| [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
|
||||
| [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) | | |
|
||||
|
||||
|
||||
## 📋 Installation
|
||||
@@ -47,25 +48,25 @@ Before you begin, make sure your device meets the system requirements:
|
||||
|
||||
Choose one of the installation methods below:
|
||||
|
||||
<details>
|
||||
<details open>
|
||||
<summary>Download .exe from GitHub</summary>
|
||||
|
||||
Go to the [PowerToys GitHub releases][github-release-link], 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.95%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysUserSetup-0.94.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.94.0/PowerToysSetup-0.94.0-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.94.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.94.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.95.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -105,175 +106,179 @@ There are [community driven install methods](./doc/unofficialInstallMethods.md)
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.94 (September 2025)**
|
||||
**Version 0.95 (October 2025)**
|
||||
|
||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
**✨ Highlights**
|
||||
|
||||
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
|
||||
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
|
||||
- Mouse Utilities added a “Gliding cursor” accessibility feature to Mouse Pointer Crosshairs for single‑button cursor movement and clicking. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- The installer was upgraded to WiX 5 after WiX 3 reached end-of-life; this move improved installer security, reliability, and community support.
|
||||
- Tons of bug fixes and improvements for Command Palette, including visual updates and new support for filters on ListPages (handy for extension developers).
|
||||
- Hosts Editor now has a “No leading spaces” option so active host entries can start at column 0 even if others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
- Context menu registration was moved from the installer to runtime to avoid loading disabled modules (runtime registrations).
|
||||
- Quick Accent now supports Maltese, and frequently used accents appear first (and are remembered across sessions). Thanks [@rovercoder](https://github.com/rovercoder)! [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### Always On Top
|
||||
|
||||
- Fixed the border hover cursor so it shows the arrow instead of the wait cursor. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- **NEW:** The **Light Switch** utility in PowerToys allows you to automatically switch between light and dark themes in Windows based on the time of day.
|
||||
- Command Palette delivered major search performance gains (new fuzzy matcher and smarter fallbacks) improving relevance and speed.
|
||||
- Peek can now be activated using just the Spacebar!
|
||||
- Find My Mouse added transparent spotlight with independent backdrop opacity, boosting focus and accessibility.
|
||||
- Settings now lets you delete shortcuts entirely and ignore conflicts.
|
||||
- Mouse Pointer Crosshairs gained orientation options (vertical / horizontal / both) for customizable accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- PowerRename fixed enumeration counter skipping ensuring reliable batch renames. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- ZoomIt restored legacy draw and snipping behaviors, and fixed recording issues, improving reliability. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
|
||||
### Command Palette
|
||||
- Applied conditional margin for icon-only tags to tighten layout. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Improved the reliability of accessing Command Palette settings through PowerToys Settings and executing other x-cmdpal:// protocol commands. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Enabled AOT by default for improved performance while simplifying publish configs.
|
||||
- Replaced service state color dots with play/pause/stop icons for enhanced accessibility. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Fixed filter dropdown sync and crash by binding SelectedValue and raising UI-thread notifications. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured long links wrap correctly in details view.
|
||||
- Removed animation and enforced minimum width on filter dropdown for clarity. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored focus to More button after ESC closes context menu, improving keyboard flow. Thanks [@chatasweetie](https://github.com/chatasweetie)
|
||||
- Marked main and toast windows as tool windows to keep them out of Alt+Tab while preserving style. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed AOT template and theming issues for filter separators. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Introduced grid layouts (small, medium, gallery) for richer page presentation.
|
||||
- Materialized result lists to avoid rescoring overhead.
|
||||
- Disabled problematic selection TextToSuggest behind environment flag.
|
||||
- Major search performance improvements (new fuzzy matcher, smarter fallbacks, fewer exceptions).
|
||||
- Added context menu "Show Details" command when details pane is hidden.
|
||||
- Reduced window flicker by avoiding unnecessary cloaking. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored EmptyContent rendering for blank states. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)
|
||||
- Saved new state even if prior app state file was corrupt (better resilience). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Migrated settings window to WinUI TitleBar control. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Prevented crash on duplicate keybindings and simplified matching. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hotkeys now always respect the “Ignore shortcut in fullscreen” setting. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hid search box on content pages, improving focus and accessibility, and added Home title. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Blocked Ctrl+I from inserting stray tabs in search box.
|
||||
- Logged HRESULT codes in error logs for deeper diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Advanced font and emoji icon classification and alignment improvements. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured that fallback command icons are visible on the extension settings page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed breadcrumb margin misalignment (visual polish). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Truncated overly long command labels with ellipsis to prevent overflow.
|
||||
- Added a setting to configure the page transition animation.
|
||||
- Collection of small improvements and nits for Run Commands.
|
||||
- Improved bookmarks performance and experience. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Ctrl+O shortcut in Clipboard History to open links directly.
|
||||
- Resolved conflict with external software that blocked Command Palette from hiding.
|
||||
- Updated context menu items to reflect name and icon changes, and ensured application icons are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Alt+Home shortcut to return immediately to the Command Palette home page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed a crash when displaying code blocks in markdown on detail or content pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed an issue where the search bar icon and title were not updated when rapidly switching pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Improved the appearance of the search box in the context menu.
|
||||
|
||||
- Applied single-click activation only to pointer input; keyboard always activates immediately. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Let context menus open at the cursor by removing window-bound constraints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made error messages clearer with timestamps, HRESULTs, and full details for easier diagnosis. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Prevented crashes and improved robustness when updating providers without commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured the Settings window reliably comes to the front when opened. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Replaced the Clipboard History icon with a colorful Fluent icon. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Hardened ContentIcon to avoid duplicate parenting and improve diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Standardized null checks using C# pattern matching for safer behavior.
|
||||
- Improved accessibility by focusing the activation shortcut dialog and making text reachable. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||
- Moved the extension SDK to a stable Windows SDK and cleaned up message namespaces.
|
||||
- Added path shortcuts: ~ to home, and / or \\ to system root, plus UNC support. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed a race in cancellation handling to avoid InvalidOperationException. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Aligned separator styling with WinUI 3 for consistent visuals. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added ARM64 PDBs to the Extensions SDK NuGet for better debugging.
|
||||
- Added single-select filters to DynamicListPage and updated Windows Services sample.
|
||||
- Updated main page placeholder text to better describe what can be searched. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Removed explicit WinAppSDK/WebView2 dependencies from toolkit and API. Thanks [@rluengen](https://github.com/rluengen)!
|
||||
- Added a local keyboard hook to handle the GoBack key reliably. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Propagated alias changes safely and resolved conflicts across view models.
|
||||
- Allowed providers to override Dispose with a virtual method.
|
||||
- Fixed memory leaks by cleaning up removed or cancelled list items.
|
||||
- Sorted DateTime extension results by relevance for better usability.
|
||||
- Reduced search text "jiggling" by avoiding redundant change notifications.
|
||||
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
|
||||
- Preserved Adaptive Card action types during trimming via DynamicDependency.
|
||||
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Made the extension API easier to evolve without breaking clients.
|
||||
- Added "evil" sample pages to help reproduce tricky bugs.
|
||||
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
|
||||
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
|
||||
|
||||
### Command Palette extensions
|
||||
### Command Palette Extensions
|
||||
- Replaced localized WebSearch setting keys with stable literals and numeric history count. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled advanced markdown tables and emphasis extensions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added setting to choose Clipboard History primary action (Paste vs Copy). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added actionable empty-state hints for File Search (search PC / open indexing settings). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured all WinGet extension assets copy reliably to output. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved Run command line parsing for paths with spaces; sped up related tests.
|
||||
- Updated WebSearch extension icon set for enhanced clarity and contrast. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal profile sort order setting including MRU tracking. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Uninstall Application command (UWP direct, Win32 via Settings). Thanks [@mKpwnz](https://github.com/mKpwnz)!
|
||||
- Deferred WinGet details loading and added timing logs.
|
||||
- Removed LINQ from All Apps extension for performance.
|
||||
- Added standardized key chord system + shortcuts to File Search commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal channel filter & remembered selection option. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled loading local/data/app images in markdown with sizing hints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added external extension reload via x-cmdpal://reload (configurable). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Instant WebSearch history updates with in-memory store & events. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added keep-after-paste option and safe delete with confirmation for Clipboard History. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
- Improved empty states and ranking logic for multiple extensions. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Added app icons to the All Apps "Run" context command when available.
|
||||
- Restored missing builtin icons by standardizing extension dependencies.
|
||||
- Unblocked local deployment by adding WinAppSDK to two sample extensions.
|
||||
### Environment Variables
|
||||
- Replaced custom window chrome with WinUI TitleBar for cleaner, maintainable Environment Variables UI.
|
||||
|
||||
### File Locksmith
|
||||
- Adopted WinUI TitleBar to simplify window chrome while preserving appearance.
|
||||
|
||||
### Find My Mouse
|
||||
- Added transparent spotlight support with separate backdrop opacity; migrated to Windows App SDK composition APIs.
|
||||
|
||||
### Hosts File Editor
|
||||
- Migrated to native WinUI TitleBar for cleaner, maintainable window chrome.
|
||||
|
||||
- Added a "No leading spaces" option so active hosts entries can start at column 0 even when others are disabled. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
### Light Switch
|
||||
- Introduced as a brand-new PowerToy module.
|
||||
- Automatically switches between light and dark themes.
|
||||
- Supports time-based scheduling or location-based sunrise/sunset switching.
|
||||
- Supports using a keyboard shortcut to force a change.
|
||||
- Supports filtering changes for Apps and/or System Theme.
|
||||
|
||||
### Image Resizer
|
||||
|
||||
- Fixed Image Resizer localization by installing satellite resources under the WinUI 3 apps culture path.
|
||||
|
||||
### Mouse Utilities
|
||||
|
||||
- Introduced "Gliding cursor" to control the pointer and click with a single hotkey for better accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
### Mouse Pointer Crosshairs
|
||||
- Added Esc key to cancel active gliding cursor sequence. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added orientation option (vertical / horizontal / both) for crosshairs customization. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Mouse Without Borders
|
||||
- Continued Common class refactor (part 5/7) by extracting clipboard and init/cleanup logic into focused classes. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
- Blocked Easy Mouse from switching machines during fullscreen apps, with an allow-list for exceptions. Thanks [@dot-tb](https://github.com/dot-tb)!
|
||||
- Fix connection failures caused by conflicting MachineId across machines. Thanks [@noraa-junker](https://github.com/noraa-junker) for troubleshooting!
|
||||
|
||||
### Peek
|
||||
|
||||
- Added Visual Studio shared project file types to XML preview and fixed bgcode handler registration. Thanks [@rezanid](https://github.com/rezanid)!
|
||||
- Fixes bgcode preview handler registration and events for reliable previews. Thanks [@pedrolamas](https://github.com/pedrolamas)!
|
||||
- Added the option to activate Peek with just the Spacebar.
|
||||
|
||||
### PowerRename
|
||||
|
||||
- Changed the Explorer accelerator key to PowErRename to avoid clashing with the New menu. Thanks [@aaron-ni](https://github.com/aaron-ni)!
|
||||
- Fixed enumeration counter skipping when regex replacement equals original filename (counters now advance reliably). Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Quick Accent
|
||||
- Expanded Welsh layout with acute, grave, and dieresis variants for vowels (consistent ordering). Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
|
||||
- Remembered character usage across sessions so frequently used accents appear first. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Added Maltese language support with specific characters and the Euro symbol. Thanks [@rovercoder](https://github.com/rovercoder)!
|
||||
- Reduced GPU usage issues by making the window Topmost only when the picker is visible. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
### Registry Preview
|
||||
- Migrated to native TitleBar and AppWindow APIs for cleaner window chrome.
|
||||
|
||||
### Screen Ruler
|
||||
- Fixed ARM64 crash by aligning cursor position structure to 8-byte boundary.
|
||||
|
||||
### Settings
|
||||
- Added ability to ignore specific hotkey conflicts to reduce noise.
|
||||
- Stopped creating backup directory during dry-run status checks (cleaner first-run).
|
||||
- Standardized casing and localization for ZoomIt and modules header.
|
||||
- Improved search results page accessibility and conditional module grouping.
|
||||
|
||||
- Added telemetry to track usage of the new shortcut conflict detection workflow.
|
||||
- Moved the shutdown action from the title bar to a footer menu item with confirmation. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Implemented comprehensive hotkey conflict detection with a dedicated resolution dialog.
|
||||
- Added branded visuals for Office and Copilot keys in the KeyVisual control.
|
||||
- Introduced Settings search with fuzzy matching and navigation to specific controls.
|
||||
- Corrected Spanish localization so product names like Awake remain in English across Settings and OOBE.
|
||||
- Simplified the Advanced Paste description in Settings for quicker reading and consistent capitalization. Thanks [@OldUser101](https://github.com/OldUser101)!
|
||||
- Localized conflict messages in the conflict window and dialog.
|
||||
### ZoomIt
|
||||
- Updated resource file to reflect standalone v9.01 and current copyright year. Thanks [@foxmsft](https://github.com/foxmsft)!
|
||||
- Restored legacy draw/snipping behaviors and fixed recording race conditions. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
- Added smooth image option for improved zoom quality using GDI+ for static zoom and Magnifier API for live zoom. Thanks [@markrussinovich](https://github.com/markrussinovich)!
|
||||
|
||||
### Installer
|
||||
### Documentation
|
||||
- New Microsoft Learn documentation for the Light Switch module.
|
||||
- New dev docs for the Light Switch module.
|
||||
|
||||
- Upgraded the installer to WiX 5 with silent "Files in Use" handling for smoother winget installs.
|
||||
- Switched Win10 context menu modules to runtime registration and added cleanup on uninstall to avoid stale entries.
|
||||
### Development (Area-Build & Area-Tests)
|
||||
- Allowed debug launches to continue when modules fail to load, speeding developer iteration.
|
||||
- Fixed spell checker dictionary entry (advapi) to eliminate false error.
|
||||
- Added VS Code development guide and launch configs to streamline cross-editor workflows.
|
||||
- Upgraded Windows App SDK and related dependencies to 1.8 for newer platform features.
|
||||
- Rewrote YAML comment to resolve new spell checker forbidden pattern. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Corrected solution structure by returning misplaced Common project, reducing build confusion.
|
||||
- Modernized build scripts with shared helpers and VS environment autodetection for simpler CLI builds.
|
||||
- Standardized build scripts and platform detection to improve reliability and reuse.
|
||||
- Added missing Command Palette version bump to align module release cadence.
|
||||
- Added EXECUTEDEFAULT term to dictionary to prevent regression build failures. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced nightly pre-warm pipeline and configurable MSBuild cache mode to improve CI performance.
|
||||
- Resolved CI forbidden pattern spelling complaint to keep pipelines green.
|
||||
- Added AI contributor instruction set to clarify code area expectations.
|
||||
- Added accessibility IDs to settings and FancyZones toggles, stabilizing UI tests.
|
||||
- Added automatic log collection on UI test failures to speed root cause analysis.
|
||||
- Stabilized Mouse Utils tests by switching to AccessibilityId selectors.
|
||||
- Added Screen Ruler UI test coverage to validate core measurement workflows.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Adds docs for building the installer locally and testing winget installs.
|
||||
- Fixed a broken style guide link in developer documentation. Thanks [@denizmaral](https://github.com/denizmaral)!
|
||||
|
||||
### Development
|
||||
|
||||
- Excluded test and coverage DLLs from BinSkim scans to cut false positives and speed up security analysis.
|
||||
- Simplified NOTICE maintenance by removing version numbers and filtering out Microsoft/System packages.
|
||||
- Improved NuGet dependency validation to prevent package downgrades and catch issues during restore.
|
||||
- Updated UTF.Unknown to a modern version to improve compatibility without breaking changes. Thanks [@304NotModified](https://github.com/304NotModified)!
|
||||
- Refreshed package catalog in CI before installing dependencies to prevent Linux workflow failures.
|
||||
- Refactored CmdPal tests with dependency injection and added coverage for queries and settings.
|
||||
- Added unit tests to verify Close on Enter swaps Copy/Save as expected. Thanks [@mohammed-saalim](https://github.com/mohammed-saalim)!
|
||||
- Added accessibility IDs to CmdPal UI for stable UI tests.
|
||||
- Rewrote system command tests with a new test base and cleaner patterns.
|
||||
- Added unit tests for WebSearch and Shell extensions with mockable settings.
|
||||
- Added unit tests and abstractions for Apps and Bookmarks extensions.
|
||||
- Cleans up AI-generated tests; adds meaningful query tests across extensions.
|
||||
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
|
||||
|
||||
## 🛣️ Roadmap
|
||||
|
||||
For [v0.95][github-next-release-work], we'll work on the items below:
|
||||
|
||||
- Continued Command Palette polish
|
||||
- Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!)
|
||||
- Upgrading Keyboard Manager's editor UI
|
||||
- UI tweaking utility with day/night theme switcher
|
||||
- DSC v3 support for top utilities
|
||||
- New UI automation tests
|
||||
- Stability, bug fixes
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][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!
|
||||
|
||||
## Contributing
|
||||
## Contributing
|
||||
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so. For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
|
||||
|
||||
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
|
||||
## Code of Conduct
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
|
||||
|
||||
We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
|
||||
## Privacy Statement
|
||||
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
|
||||
|
||||
Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
|
||||
|
||||
For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
|
||||
|
||||
## Privacy Statement
|
||||
|
||||
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
|
||||
|
||||
[oss-CLA]: https://cla.opensource.microsoft.com
|
||||
[oss-conduct-code]: CODE_OF_CONDUCT.md
|
||||
[community-link]: COMMUNITY.md
|
||||
[github-release-link]: https://aka.ms/installPowerToys
|
||||
[microsoft-store-link]: https://aka.ms/getPowertoys
|
||||
[winget-link]: https://github.com/microsoft/winget-cli#installing-the-client
|
||||
[oss-CLA]: https://cla.opensource.microsoft.com
|
||||
[oss-conduct-code]: CODE_OF_CONDUCT.md
|
||||
[community-link]: COMMUNITY.md
|
||||
[github-release-link]: https://aka.ms/installPowerToys
|
||||
[microsoft-store-link]: https://aka.ms/getPowertoys
|
||||
[winget-link]: https://github.com/microsoft/winget-cli#installing-the-client
|
||||
[roadmap]: https://github.com/microsoft/PowerToys/wiki/Roadmap
|
||||
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
|
||||
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
|
||||
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
|
||||
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
|
||||
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
|
||||
|
||||
@@ -594,6 +594,216 @@ LExit:
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall InstallPackageIdentityMSIXCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
LPWSTR customActionData = nullptr;
|
||||
std::wstring installFolderPath;
|
||||
std::wstring installScope;
|
||||
std::wstring msixPath;
|
||||
std::wstring data;
|
||||
size_t delimiterPos;
|
||||
bool isMachineLevel = false;
|
||||
|
||||
hr = WcaInitialize(hInstall, "InstallPackageIdentityMSIXCA");
|
||||
ExitOnFailure(hr, "Failed to initialize");
|
||||
|
||||
hr = WcaGetProperty(L"CustomActionData", &customActionData);
|
||||
ExitOnFailure(hr, "Failed to get CustomActionData property");
|
||||
|
||||
// Parse CustomActionData: "[INSTALLFOLDER];[InstallScope]"
|
||||
data = customActionData;
|
||||
delimiterPos = data.find(L';');
|
||||
installFolderPath = data.substr(0, delimiterPos);
|
||||
installScope = data.substr(delimiterPos + 1);
|
||||
|
||||
// Check if this is a machine-level installation
|
||||
if (installScope == L"perMachine")
|
||||
{
|
||||
isMachineLevel = true;
|
||||
}
|
||||
|
||||
Logger::info(L"Installing PackageIdentity MSIX - perUser: {}", !isMachineLevel);
|
||||
|
||||
// Construct path to PackageIdentity MSIX
|
||||
msixPath = installFolderPath;
|
||||
msixPath += L"PowerToysSparse.msix";
|
||||
|
||||
if (std::filesystem::exists(msixPath))
|
||||
{
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
using namespace winrt::Windows::Foundation;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
std::wstring externalLocation = installFolderPath; // External content location (PowerToys install folder)
|
||||
Uri externalUri{ externalLocation }; // External location URI for sparse package content
|
||||
Uri packageUri{ msixPath }; // The MSIX file URI
|
||||
|
||||
PackageManager packageManager;
|
||||
|
||||
if (isMachineLevel)
|
||||
{
|
||||
// Machine-level installation
|
||||
|
||||
StagePackageOptions stageOptions;
|
||||
stageOptions.ExternalLocationUri(externalUri);
|
||||
|
||||
auto stageResult = packageManager.StagePackageByUriAsync(packageUri, stageOptions).get();
|
||||
|
||||
uint32_t stageErrorCode = static_cast<uint32_t>(stageResult.ExtendedErrorCode());
|
||||
if (stageErrorCode == 0)
|
||||
{
|
||||
std::wstring packageFamilyName = L"Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe";
|
||||
|
||||
try
|
||||
{
|
||||
auto provisionResult = packageManager.ProvisionPackageForAllUsersAsync(packageFamilyName).get();
|
||||
uint32_t provisionErrorCode = static_cast<uint32_t>(provisionResult.ExtendedErrorCode());
|
||||
|
||||
if (provisionErrorCode != 0)
|
||||
{
|
||||
Logger::error(L"Machine-level provisioning failed: 0x{:08X}", provisionErrorCode);
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::error(L"Provisioning exception: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Package staging failed: 0x{:08X}", stageErrorCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddPackageOptions addOptions;
|
||||
addOptions.ExternalLocationUri(externalUri);
|
||||
|
||||
auto addResult = packageManager.AddPackageByUriAsync(packageUri, addOptions).get();
|
||||
|
||||
if (!addResult.IsRegistered())
|
||||
{
|
||||
uint32_t errorCode = static_cast<uint32_t>(addResult.ExtendedErrorCode());
|
||||
Logger::error(L"Per-user installation failed: 0x{:08X}", errorCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
Logger::error(L"PackageIdentity MSIX installation failed - Exception: {}",
|
||||
winrt::to_hstring(ex.what()).c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"PackageIdentity MSIX not found: " + msixPath);
|
||||
}
|
||||
|
||||
LExit:
|
||||
ReleaseStr(customActionData);
|
||||
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall UninstallPackageIdentityMSIXCA(MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UINT er = ERROR_SUCCESS;
|
||||
using namespace winrt::Windows::Management::Deployment;
|
||||
using namespace winrt::Windows::Foundation;
|
||||
|
||||
LPWSTR installScope = nullptr;
|
||||
bool isMachineLevel = false;
|
||||
|
||||
PackageManager pm;
|
||||
|
||||
hr = WcaInitialize(hInstall, "UninstallPackageIdentityMSIXCA");
|
||||
ExitOnFailure(hr, "Failed to initialize");
|
||||
|
||||
// Check if this was a machine-level installation
|
||||
hr = WcaGetProperty(L"InstallScope", &installScope);
|
||||
if (SUCCEEDED(hr) && installScope && wcscmp(installScope, L"perMachine") == 0)
|
||||
{
|
||||
isMachineLevel = true;
|
||||
}
|
||||
|
||||
Logger::info(L"Uninstalling PackageIdentity MSIX - perUser: {}", !isMachineLevel);
|
||||
|
||||
try
|
||||
{
|
||||
std::wstring packageFamilyName = L"Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe";
|
||||
|
||||
if (isMachineLevel)
|
||||
{
|
||||
// Machine-level uninstallation: deprovision + remove for all users
|
||||
|
||||
// First deprovision the package
|
||||
try
|
||||
{
|
||||
auto deprovisionResult = pm.DeprovisionPackageForAllUsersAsync(packageFamilyName).get();
|
||||
if (deprovisionResult.IsRegistered())
|
||||
{
|
||||
Logger::warn(L"Machine-level deprovisioning completed with warnings");
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::warn(L"Machine-level deprovisioning failed: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
|
||||
}
|
||||
|
||||
// Then remove packages for all users
|
||||
auto packages = pm.FindPackagesForUserWithPackageTypes({}, packageFamilyName, PackageTypes::Main);
|
||||
for (const auto& package : packages)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto machineResult = pm.RemovePackageAsync(package.Id().FullName(), RemovalOptions::RemoveForAllUsers).get();
|
||||
if (machineResult.IsRegistered())
|
||||
{
|
||||
uint32_t errorCode = static_cast<uint32_t>(machineResult.ExtendedErrorCode());
|
||||
Logger::error(L"Machine-level removal failed: 0x{:08X} - {}", errorCode, machineResult.ErrorText());
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
Logger::error(L"Machine-level removal exception: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Per-user uninstallation: standard removal
|
||||
|
||||
auto packages = pm.FindPackagesForUserWithPackageTypes({}, packageFamilyName, PackageTypes::Main);
|
||||
for (const auto& package : packages)
|
||||
{
|
||||
auto userResult = pm.RemovePackageAsync(package.Id().FullName()).get();
|
||||
if (userResult.IsRegistered())
|
||||
{
|
||||
uint32_t errorCode = static_cast<uint32_t>(userResult.ExtendedErrorCode());
|
||||
Logger::error(L"Per-user removal failed: 0x{:08X} - {}", errorCode, userResult.ErrorText());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
std::string errorMsg = "Failed to uninstall PackageIdentity MSIX: " + std::string(ex.what());
|
||||
Logger::error(errorMsg);
|
||||
// Don't fail the entire uninstallation if PackageIdentity fails
|
||||
Logger::warn(L"Continuing uninstallation despite PackageIdentity MSIX error");
|
||||
}
|
||||
|
||||
LExit:
|
||||
ReleaseStr(installScope);
|
||||
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
UINT __stdcall RemoveWindowsServiceByName(std::wstring serviceName)
|
||||
{
|
||||
SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
|
||||
|
||||
@@ -33,3 +33,5 @@ EXPORTS
|
||||
CleanPowerRenameRuntimeRegistryCA
|
||||
CleanNewPlusRuntimeRegistryCA
|
||||
SetBundleInstallLocationCA
|
||||
InstallPackageIdentityMSIXCA
|
||||
UninstallPackageIdentityMSIXCA
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
<OutDir Condition=" '$(PerUser)' == 'true' ">$(Platform)\$(Configuration)\UserSetup\</OutDir>
|
||||
<IntDir Condition=" '$(PerUser)' != 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\MachineSetup\obj\</IntDir>
|
||||
<IntDir Condition=" '$(PerUser)' == 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\UserSetup\obj\</IntDir>
|
||||
<!-- The CMD script below checks this value, and it is **CASE SENSITIVE** -->
|
||||
<NormalizedPerUserValue>false</NormalizedPerUserValue>
|
||||
<NormalizedPerUserValue Condition=" '$(PerUser)' == 'true' ">true</NormalizedPerUserValue>
|
||||
</PropertyGroup>
|
||||
@@ -115,7 +114,6 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -128,7 +126,6 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -9,6 +9,25 @@
|
||||
<RegistryValue Type="string" Name="InstallScope" Value="$(var.InstallScope)" />
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<Component Id="powertoys_env_path_user" Bitness="always64">
|
||||
<!-- Anchor registry for component key path -->
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="powertoys_env_path_user" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<!-- Append install folder to current user's PATH -->
|
||||
<Environment Id="AddPowerToysToUserPath" Name="PATH" Action="set" Part="last" System="no" Value="[INSTALLFOLDER]" />
|
||||
</Component>
|
||||
<?else?>
|
||||
<Component Id="powertoys_env_path_machine" Bitness="always64">
|
||||
<!-- Anchor registry for component key path -->
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="powertoys_env_path_machine" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<!-- Append install folder to machine PATH -->
|
||||
<Environment Id="AddPowerToysToMachinePath" Name="PATH" Action="set" Part="last" System="yes" Value="[INSTALLFOLDER]" />
|
||||
</Component>
|
||||
<?endif?>
|
||||
<Component Id="powertoys_toast_clsid" Bitness="always64">
|
||||
<RemoveFolder Id="Remove_powertoys_toast_clsid" On="uninstall" />
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{DD5CACDA-7C2E-4997-A62A-04A597B58F76}">
|
||||
@@ -109,6 +128,11 @@
|
||||
<ComponentRef Id="powertoys_exe" />
|
||||
<ComponentRef Id="PowerToysStartMenuShortcut" />
|
||||
<ComponentRef Id="powertoys_per_machine_comp" />
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<ComponentRef Id="powertoys_env_path_user" />
|
||||
<?else?>
|
||||
<ComponentRef Id="powertoys_env_path_machine" />
|
||||
<?endif?>
|
||||
<ComponentRef Id="powertoys_toast_clsid" />
|
||||
<ComponentRef Id="License_rtf" />
|
||||
<ComponentRef Id="Notice_md" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<Project>
|
||||
<Import Project="..\..\Directory.Build.props" Condition="Exists('..\..\Directory.Build.props')" />
|
||||
<Import Project="..\..\src\Version.props" Condition="Exists('..\..\src\Version.props')" />
|
||||
<PropertyGroup>
|
||||
<!-- Set BaseIntermediateOutputPath for each project to avoid conflicts -->
|
||||
@@ -8,4 +9,4 @@
|
||||
<!-- Set MSBuildProjectExtensionsPath to use the BaseIntermediateOutputPath -->
|
||||
<MSBuildProjectExtensionsPath Condition="'$(BaseIntermediateOutputPath)' != ''">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -112,6 +112,7 @@
|
||||
<Custom Action="SetUninstallCommandNotFoundParam" Before="UninstallCommandNotFound" />
|
||||
<Custom Action="SetUpgradeCommandNotFoundParam" Before="UpgradeCommandNotFound" />
|
||||
<Custom Action="SetApplyModulesRegistryChangeSetsParam" Before="ApplyModulesRegistryChangeSets" />
|
||||
<Custom Action="SetInstallPackageIdentityMSIXParam" Before="InstallPackageIdentityMSIX" />
|
||||
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<Custom Action="SetInstallDSCModuleParam" Before="InstallDSCModule" />
|
||||
@@ -123,6 +124,7 @@
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
|
||||
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
|
||||
<!-- TODO: Use to activate embedded MSIX -->
|
||||
@@ -144,6 +146,7 @@
|
||||
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||
<!-- TODO: Use to activate embedded MSIX -->
|
||||
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">
|
||||
@@ -199,6 +202,12 @@
|
||||
|
||||
<CustomAction Id="UninstallEmbeddedMSIXTask" Return="ignore" Impersonate="yes" DllEntry="UninstallEmbeddedMSIXCA" BinaryRef="PTCustomActions" />
|
||||
|
||||
<CustomAction Id="SetInstallPackageIdentityMSIXParam" Property="InstallPackageIdentityMSIX" Value="[INSTALLFOLDER];[InstallScope]" />
|
||||
|
||||
<CustomAction Id="InstallPackageIdentityMSIX" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallPackageIdentityMSIXCA" BinaryRef="PTCustomActions" />
|
||||
|
||||
<CustomAction Id="UninstallPackageIdentityMSIX" Return="ignore" Impersonate="yes" DllEntry="UninstallPackageIdentityMSIXCA" BinaryRef="PTCustomActions" />
|
||||
|
||||
<CustomAction Id="InstallDSCModule" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallDSCModuleCA" BinaryRef="PTCustomActions" />
|
||||
|
||||
<CustomAction Id="UninstallDSCModule" Return="ignore" Impersonate="yes" DllEntry="UninstallDSCModuleCA" BinaryRef="PTCustomActions" />
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
||||
|
||||
<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="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</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">
|
||||
<ProjectGuid>{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}</ProjectGuid>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<TargetName>SilentFilesInUseBAFunction</TargetName>
|
||||
<ProjectName>PowerToysSetupCustomActionsVNext</ProjectName>
|
||||
<ProjectModuleDefinitionFile>bafunctions.def</ProjectModuleDefinitionFile>
|
||||
@@ -33,7 +10,6 @@
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<!-- Configuration-specific property groups -->
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
@@ -65,7 +41,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClCompile Include="SilentFilesInUseBAFunctions.cpp" />
|
||||
<ClCompile Include="SilentFilesInUseBAFunctions.cpp">
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ClCompile Include="bafunctions.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
|
||||
@@ -92,31 +71,5 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- C++ source compile-specific things for Debug/Release configurations -->
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -18,7 +18,6 @@ public: // IBootstrapperApplication
|
||||
|
||||
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running detect begin BA function. fCached=%d, registrationType=%d, cPackages=%u, fCancel=%d", fCached, registrationType, cPackages, *pfCancel);
|
||||
|
||||
LExit:
|
||||
return hr;
|
||||
}
|
||||
|
||||
@@ -32,12 +31,6 @@ public: // IBAFunctions
|
||||
|
||||
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running plan begin BA function. cPackages=%u, fCancel=%d", cPackages, *pfCancel);
|
||||
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
// YOUR CODE GOES HERE
|
||||
// BalExitOnFailure(hr, "Change this message to represent real error handling.");
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
|
||||
LExit:
|
||||
return hr;
|
||||
}
|
||||
|
||||
@@ -63,6 +56,7 @@ public: // IBAFunctions
|
||||
)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
UNREFERENCED_PARAMETER(source);
|
||||
|
||||
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION CALLED *** Running OnExecuteFilesInUse BA function. packageId=%ls, cFiles=%u, recommendation=%d", wzPackageId, cFiles, nRecommendation);
|
||||
|
||||
|
||||
59
pt-cli-plan.md
Normal file
59
pt-cli-plan.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# PowerToys CLI Implementation Plan
|
||||
|
||||
## Goal
|
||||
- Deliver the `ptcli` command-line experience described in `pt-cli.md`, with `Runner` acting as the single broker for module commands.
|
||||
- Provide a maintainable architecture where modules self-describe commands, and CLI clients consume a uniform JSON/NamedPipe protocol.
|
||||
|
||||
## Workstreams
|
||||
|
||||
### 1. Broker Foundation (Runner)
|
||||
- **Command Registry**: Implement `IModuleCommandProvider` registration on module load and persist `CommandDescriptor` metadata (schema, elevation flag, long-running hints, docs).
|
||||
- **IPC Host**: Stand up the `\\.\pipe\PowerToys.Runner.CLI` NamedPipe server; define request/response DTOs with versioning (`v` field, correlation IDs).
|
||||
- **Dispatch Pipeline**: Validate module/action, apply schema validation, enforce elevation policy, and invoke `ExecuteAsync`.
|
||||
- **Response Envelope**: Normalize `status` (`ok|error|accepted`), payload, and error block (`code/message/details`). Emit diagnostic logging (caller, command, latency, result).
|
||||
|
||||
### 2. CLI Thin Client (`ptcli`)
|
||||
- **Argument Parsing**: Support `ptcli -m <module> <action> [--arg value]`, plus `--list-modules`, `--list-commands`.
|
||||
- **Transport**: Serialize requests to JSON, connect to the pipe with timeout handling, and deserialize responses.
|
||||
- **Output UX**: Map standard errors to friendly text, show structured results, and support optional `--json` passthrough.
|
||||
- **Async Jobs**: Handle `status=accepted` by printing job IDs, exposing `ptcli job status <id>` and `ptcli job cancel <id>` commands (polling via Runner endpoints).
|
||||
|
||||
### 3. Module Onboarding
|
||||
- **Awake**: Implement `IModuleCommandProvider` returning `set/start/stop/list` commands. Adapt current APIs or legacy triggers inside `ExecuteAsync`.
|
||||
- **Workspaces**: Provide `list/apply/delete` commands; wrap existing workspace manager calls. Ensure long-running operations flag `LongRunning=true`.
|
||||
- **Legacy Adapters**: For modules still using raw events/pipes, add Runner-side shims that translate command invocations while longer-term refactors are scheduled.
|
||||
|
||||
### 4. Capability Discovery & Help
|
||||
- **Describe APIs**: Expose Runner endpoints for `modules`, `commands`, parameter schemas, and elevation requirements.
|
||||
- **CLI Help**: Use discovery data to render `ptcli help`, module-specific usage, and argument hints without duplicating metadata.
|
||||
|
||||
### 5. Reliability, Security, Observability
|
||||
- **Security**: Configure pipe DACL to restrict access to the interactive user; enforce argument length/type limits.
|
||||
- **Concurrency**: Process each request on a dedicated task; delegate concurrency limits to modules. Provide cancellation tokens from Runner.
|
||||
- **Tracing**: Emit structured logs/ETW for requests, errors, and long-running progress notifications.
|
||||
- **Error Catalog**: Implement standardized error codes (`E_MODULE_NOT_FOUND`, `E_ARGS_INVALID`, `E_NEEDS_ELEVATION`, `E_TIMEOUT`, etc.) and map module exceptions accordingly.
|
||||
|
||||
### 6. Elevation & Policies
|
||||
- **Elevation Flow**: Detect when commands require elevation; if Runner is not elevated, return `E_NEEDS_ELEVATION` with actionable hints. Integrate with existing elevated Runner helper when available.
|
||||
- **Policy Hooks**: Add optional checks for policy/experiment gates before command execution.
|
||||
|
||||
### 7. Progress & Notifications
|
||||
- **Progress Channel**: Support incremental JSON progress messages over the same pipe or via job polling endpoints.
|
||||
- **Timeouts/Retry**: Implement configurable `timeoutMs` handling and `E_BUSY_RETRY` responses for transient module lock scenarios.
|
||||
|
||||
### 8. Incremental Rollout Strategy
|
||||
- **Phase 1**: Ship Runner pipe host + CLI client with two flagship commands (Awake.Set, Workspaces.List); document manual enablement.
|
||||
- **Phase 2**: Migrate additional modules through adapters; add help/describe surfaces and job management.
|
||||
- **Phase 3**: Enforce schema validation, finalize error catalog, and wire observability dashboards.
|
||||
- **Phase 4**: Deprecate direct module NamedPipe/event entry points once CLI parity is achieved.
|
||||
|
||||
### 9. Documentation & Maintenance
|
||||
- **User Docs**: Populate `pt-cli.md` with usage examples, elevation guidance, and troubleshooting mapped to error codes.
|
||||
- **Developer Guide**: Add module author instructions for implementing `IModuleCommandProvider`, including schema examples and best practices.
|
||||
- **Release Checklist**: Track new commands per release, update discovery metadata, and ensure CLI integration tests cover regression cases.
|
||||
|
||||
## Open Questions
|
||||
- What tooling will maintain JSON schemas (hand-authored vs. source-generated)?
|
||||
- Should progress streaming use duplex pipe messages or a per-job polling API?
|
||||
- How will elevated Runner lifecycle be managed (reuse existing helper vs. new broker)?
|
||||
- Which modules are in-scope for the first public preview, and what is the rollout schedule?
|
||||
358
pt-cli.md
Normal file
358
pt-cli.md
Normal file
@@ -0,0 +1,358 @@
|
||||
选 Runner 作为唯一的 Server/Broker,ptcli 只是瘦客户端。
|
||||
|
||||
|
||||
模块通过各自的 ModuleInterface 向 Runner 注册“可被调用的命令/参数模式”。
|
||||
|
||||
|
||||
ptcli → Runner 用 统一的 IPC(建议 NamedPipe + JSON-RPC/自定义轻量 JSON 协议)。
|
||||
|
||||
|
||||
Runner 再把请求转发到对应模块(可以是直接调用模块公开的接口,或转译为该模块现有的触发机制,如 Event/NamedPipe)。
|
||||
|
||||
|
||||
对“历史遗留的 event handle/pipe 触发点”,短期由 Runner 做兼容层;长期逐步统一为“命令接口”。
|
||||
|
||||
|
||||
这样你能得到:能力发现、参数校验、权限/提权、错误码一致、可观察性一致、向后兼容。
|
||||
|
||||
|
||||
|
||||
组件与职责
|
||||
|
||||
|
||||
ptcli(瘦客户端)
|
||||
|
||||
|
||||
|
||||
|
||||
解析命令行:ptcli -m awake set --duration 1h / ptcli -m workspace list
|
||||
|
||||
|
||||
将其映射为通用消息(JSON)发给 Runner。
|
||||
|
||||
|
||||
处理同步/异步返回、展示统一错误码与人类可读信息。
|
||||
|
||||
|
||||
最多内置“列出模块/命令的帮助”这类“离线功能”,但真正的能力发现来自 Runner。
|
||||
|
||||
|
||||
|
||||
|
||||
Runner(统一 Server/Broker)
|
||||
|
||||
|
||||
|
||||
|
||||
启动时建立 NamedPipe 服务端:\\.\pipe\PowerToys.Runner.CLI(示例)。
|
||||
|
||||
|
||||
维护 Command Registry:每个模块在加载/初始化时注册自己的命令(名称、参数 schema、是否需要提权、是否长任务、超时时间建议、描述文案等)。
|
||||
|
||||
|
||||
收到请求后:
|
||||
|
||||
|
||||
校验模块是否存在、命令是否存在、参数是否通过 schema 验证。
|
||||
|
||||
|
||||
如需提权且当前 Runner 权限不足:按策略返回“需要提权”的标准错误,或通过你们现有的提权助手启动“Elevated Runner”做代办。
|
||||
|
||||
|
||||
转发给目标模块(优先调用模块公开的“命令接口方法”;若模块尚未改造,由 Runner 适配为该模块现有触发(Event/NamedPipe))。
|
||||
|
||||
|
||||
汇总返回值,统一封装标准响应(状态、数据、错误码、诊断信息)。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Module(实现者)
|
||||
|
||||
|
||||
|
||||
|
||||
实现 IModuleCommandProvider(示例命名):
|
||||
|
||||
|
||||
IEnumerable<CommandDescriptor> DescribeCommands() 暴露命令元数据;
|
||||
|
||||
|
||||
Task<CommandResult> ExecuteAsync(CommandInvocation ctx) 执行命令;
|
||||
|
||||
|
||||
可标注“需要前台 UI”、“需要管理员”、“可能长时间运行(支持取消/进度)”等。
|
||||
|
||||
|
||||
|
||||
|
||||
现有“事件/NamedPipe 触发路径”的模块:短期由 Runner 适配;长期建议模块直接实现上面的 ExecuteAsync,统一语义与可观测性。
|
||||
|
||||
|
||||
|
||||
协议与数据结构(建议)
|
||||
请求(ptcli→Runner)
|
||||
{
|
||||
"v": 1,
|
||||
"correlationId": "uuid",
|
||||
"command": {
|
||||
"module": "awake",
|
||||
"action": "set", // 例如 set/start/stop/list 等
|
||||
"args": { "duration": "1h" } // 按模块定义的 schema
|
||||
},
|
||||
"options": {
|
||||
"timeoutMs": 20000,
|
||||
"wantProgress": false
|
||||
}
|
||||
}
|
||||
|
||||
响应(Runner→ptcli)
|
||||
{
|
||||
"v": 1,
|
||||
"correlationId": "uuid",
|
||||
"status": "ok", // ok | error | accepted (异步)
|
||||
"result": { /* 模块返回的结构化数据 */ },
|
||||
"error": { // 仅当 status=error
|
||||
"code": "E_NEEDS_ELEVATION", // 标准化错误码
|
||||
"message": "Awake requires elevation",
|
||||
"details": { "hint": "rerun with --elevated" }
|
||||
}
|
||||
}
|
||||
|
||||
进度/异步(可选)
|
||||
|
||||
|
||||
长任务时,status="accepted" 并返回 jobId;ptcli 可 ptcli job status <jobId> 轮询,或 Runner 通过同管道 增量推送 progress(JSON lines)。
|
||||
|
||||
|
||||
取消:ptcli 发送 { action: "cancel", jobId: "..." },Runner 调用模块 CancellationToken。
|
||||
|
||||
|
||||
|
||||
命令发现与帮助
|
||||
|
||||
|
||||
ptcli -m list:列出模块(Runner 直接返回 registry)。
|
||||
|
||||
|
||||
ptcli -m awake -h:DescribeCommands() 中的 Awake 条目返回所有 action、参数与示例。
|
||||
|
||||
|
||||
参数 schema:用简化 JSON Schema(或手写约束)即可,让 ptcli 能本地提示,也让 Runner 能服务器端校验。
|
||||
|
||||
|
||||
|
||||
示例映射
|
||||
Awake
|
||||
|
||||
|
||||
ptcli -m awake set --duration 1h
|
||||
→ Awake.Set(duration=1h)
|
||||
→ Runner 调用 AwakeModule.ExecuteAsync("set", args)
|
||||
→ 结果:{ "effectiveUntil": "2025-10-30T18:00:00+08:00" }
|
||||
|
||||
|
||||
ptcli -m awake stop
|
||||
→ Awake.Stop()(幂等)
|
||||
|
||||
|
||||
Workspaces
|
||||
|
||||
|
||||
ptcli -m workspace list
|
||||
→ Workspaces.List() 返回 { "items": [{ "id": "...", "name": "...", "monitors": 2, "windows": 14 }] }
|
||||
|
||||
|
||||
ptcli -m workspace apply --id 123 --strict
|
||||
→ Workspaces.Apply(id=123, strict=true) 支持进度与失败报告(缺失进程、权限不足等)。
|
||||
|
||||
|
||||
|
||||
Runner vs 直接敲模块事件/NamedPipe
|
||||
直接敲模块(如你举的 EventWaitHandle)优点
|
||||
|
||||
|
||||
省一跳,模块自己掌控。
|
||||
|
||||
|
||||
对少数“拍一下就够”的快捷触发点,写起来快。
|
||||
|
||||
|
||||
缺点(关键)
|
||||
|
||||
|
||||
入口分散:每个模块各有各的触发名、参数约定、错误语义。
|
||||
|
||||
|
||||
能力发现困难:ptcli 无法统一列出“模块能干啥、参数是什么”。
|
||||
|
||||
|
||||
权限与多实例问题:有的模块需要管理员/前台,有的在用户会话,有的在服务;直接对模块打洞容易踩坑。
|
||||
|
||||
|
||||
审计/可观察性差:难以统一日志/遥测/超时/取消。
|
||||
|
||||
|
||||
演进成本高:接口一旦铺散,很难回收。
|
||||
|
||||
|
||||
走 Runner Proxy(推荐)
|
||||
|
||||
|
||||
统一注册:模块只跟 Runner 说“我能做哪些命令、参数是什么”。
|
||||
|
||||
|
||||
统一协议:ptcli 只会说一种“通用 JSON 命令”。
|
||||
|
||||
|
||||
统一安全/提权/会话:Runner 最懂自己所在的权限/桌面会话,可决定是否需要跳 Elevation/切用户会话。
|
||||
|
||||
|
||||
兼容旧触发:Runner 内部去“Set 事件/写管道”,外部对 ptcli 完全透明。
|
||||
|
||||
|
||||
可测试/可监控:所有调用都经由同一 Broker,便于打点、限流、诊断。
|
||||
|
||||
|
||||
|
||||
结论:把直接事件/管道触发视为“模块侧 private API”,只由 Runner 调用。ptcli 与普通用户两边都只看得到 Runner 的“公共命令接口”。
|
||||
|
||||
|
||||
Runner 怎么“轻量 Server”
|
||||
|
||||
|
||||
进程:沿用现有 Runner,不另起新守护;新增一个 CommandRouter 子系统即可。
|
||||
|
||||
|
||||
IPC:NamedPipeServerStream + StreamJsonRpc(或你们已有的 JSON 框架);单管道多请求(长度前缀 + correlationId)。
|
||||
|
||||
|
||||
并发:每请求一个 Task,模块执行受自身并发控制。
|
||||
|
||||
|
||||
安全:给管道设定 DACL,仅允许同一交互式用户(或受信 SID)连接;参数白名单与长度限制防注入。
|
||||
|
||||
|
||||
错误码:统一枚举(像 HTTP 状态一样):
|
||||
|
||||
|
||||
E_MODULE_NOT_FOUND / E_COMMAND_NOT_FOUND / E_ARGS_INVALID
|
||||
|
||||
|
||||
E_NEEDS_ELEVATION / E_ACCESS_DENIED
|
||||
|
||||
|
||||
E_BUSY_RETRY / E_TIMEOUT / E_INTERNAL
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
最小可行落地(增量实施顺序)
|
||||
|
||||
|
||||
在 Runner 加一个 Pipe + CommandRouter,硬编码两个演示命令:
|
||||
|
||||
|
||||
Awake.Set(duration)(直接调用 Awake 的现有 API)
|
||||
|
||||
|
||||
Workspaces.List()(调用 Workspace 管理器)
|
||||
|
||||
|
||||
|
||||
|
||||
写 ptcli:只做 JSON 打包、发管道、打印结果。
|
||||
|
||||
|
||||
给两个模块各加 IModuleCommandProvider,从 Runner 注册。
|
||||
|
||||
|
||||
把 1~2 个“历史事件触发点”接入 Router(Runner 内部去 Set Event),对外暴露为 Module.Action。
|
||||
|
||||
|
||||
扩展:help/describe、Job/进度、取消、提权路径、返回码规范化。
|
||||
|
||||
|
||||
|
||||
简短示例(C#,仅示意;注释英文)
|
||||
Runner – 接口定义
|
||||
public record CommandDescriptor(
|
||||
string Module, string Action, string Description,
|
||||
IReadOnlyDictionary<string, ParamSpec> Params,
|
||||
bool RequiresElevation = false, bool LongRunning = false);
|
||||
|
||||
public interface IModuleCommandProvider
|
||||
{
|
||||
IEnumerable<CommandDescriptor> DescribeCommands();
|
||||
Task<CommandResult> ExecuteAsync(CommandInvocation ctx, CancellationToken ct);
|
||||
}
|
||||
|
||||
public record CommandInvocation(string Action, IReadOnlyDictionary<string, object?> Args);
|
||||
public record CommandResult(bool Ok, object? Data = null, string? ErrorCode = null, string? ErrorMessage = null);
|
||||
|
||||
Runner – 注册与路由(伪码)
|
||||
// On module load:
|
||||
registry.Register(provider.DescribeCommands(), provider);
|
||||
|
||||
// On request:
|
||||
var cmd = request.Command; // module, action, args
|
||||
var provider = registry.Resolve(cmd.Module, cmd.Action);
|
||||
ValidateArgs(cmd.Args, provider.Schema);
|
||||
if (provider.RequiresElevation && !IsElevated())
|
||||
return Error("E_NEEDS_ELEVATION", "Elevation required.");
|
||||
return await provider.ExecuteAsync(new CommandInvocation(cmd.Action, cmd.Args), ct);
|
||||
|
||||
ptcli – 调用(伪码)
|
||||
// Build request from CLI args
|
||||
var req = new { v = 1, correlationId = Guid.NewGuid(), command = new { module, action, args } };
|
||||
using var client = new NamedPipeClientStream(".", "PowerToys.Runner.CLI", PipeDirection.InOut);
|
||||
await client.ConnectAsync(timeout);
|
||||
await WriteJsonAsync(client, req);
|
||||
var resp = await ReadJsonAsync(client);
|
||||
Render(resp);
|
||||
return resp.status == "ok" ? 0 : 1;
|
||||
|
||||
|
||||
兼容性与演进策略
|
||||
|
||||
|
||||
不强行改造所有模块:Runner 先用“适配器”把旧事件/管道触发包起来,CLI 立即可用;后续模块逐步实现 IModuleCommandProvider。
|
||||
|
||||
|
||||
版本化:请求/响应带 v,便于以后扩展字段。
|
||||
|
||||
|
||||
可观测性:Runner 统一日志:调用者、命令、耗时、结果码;必要时加 ETW/Trace。
|
||||
|
||||
|
||||
权限:Runner 决策提权路径;ptcli 接到 E_NEEDS_ELEVATION 时,可提示 --elevated 或弹系统 UAC(按你们策略)。
|
||||
|
||||
|
||||
并行/队列:对长任务(如 workspace apply)可按模块内队列执行,避免竞态;CLI 侧提供 job status/cancel。
|
||||
|
||||
|
||||
|
||||
你关心的具体问题直答
|
||||
|
||||
|
||||
“由 moduleinterface 做统一入口管理是个好主意吗?”
|
||||
好,但入口管理应驻留在 Runner。模块通过 ModuleInterface 报告命令与执行入口,Runner 统一对外暴露。
|
||||
|
||||
|
||||
“ptcli 直接找模块执行,还是让 Runner 派发?”
|
||||
让 Runner 派发。这样获得统一发现、权限、错误、日志与演进空间。
|
||||
|
||||
|
||||
“现在很多模块用 named pipe/event 触发,这算绕过 Runner 吗?”
|
||||
目前你看到的例子确实是“模块直连触发”。建议以后仅 Runner 使用这些内部触发(做兼容/过渡),对外统一成命令。
|
||||
|
||||
|
||||
“Runner 起一个 light-weight server 吗?”
|
||||
对。就在 Runner 里加一个轻量 NamedPipe JSON-RPC 服务,不必单独再起一个服务进程。
|
||||
|
||||
|
||||
|
||||
要的话,我可以按上面的协议给你一版 ptcli(C#)与 Runner 侧 CommandRouter 的“最小可用代码骨架”,再加上 Awake/Workspaces 的两个真实命令适配示例。
|
||||
67
src/PackageIdentity/AppxManifest.xml
Normal file
67
src/PackageIdentity/AppxManifest.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Sparse package manifest (moved to PackageIdentity folder for cleaner organization).
|
||||
Based on Windows AI Foundry WPF sparse sample with PowerOCR customizations. -->
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2"
|
||||
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
|
||||
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10"
|
||||
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai">
|
||||
<Identity
|
||||
Name="Microsoft.PowerToys.SparseApp"
|
||||
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
Version="0.0.1.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>PowerToys.SparseApp</DisplayName>
|
||||
<PublisherDisplayName>PowerToys</PublisherDisplayName>
|
||||
<Logo>Images\StoreLogo.png</Logo>
|
||||
<uap10:AllowExternalContent>true</uap10:AllowExternalContent>
|
||||
</Properties>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="en-us" />
|
||||
</Resources>
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.26226.0" />
|
||||
</Dependencies>
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<systemai:Capability Name="systemAIModels"/>
|
||||
<rescap:Capability Name="unvirtualizedResources"/>
|
||||
</Capabilities>
|
||||
|
||||
<Applications>
|
||||
<Application Id="PowerToys.OCR" Executable="PowerToys.PowerOCR.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.OCR"
|
||||
Description="PowerToys OCR Module"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.SettingsUI"
|
||||
Description="PowerToys Settings UI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.ImageResizer"
|
||||
Description="PowerToys Image Resizer UI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
</Package>
|
||||
6
src/PackageIdentity/BuildSparsePackage.cmd
Normal file
6
src/PackageIdentity/BuildSparsePackage.cmd
Normal file
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
REM Wrapper to invoke PowerToys sparse package build script.
|
||||
REM Pass through all arguments (e.g. Platform=arm64 Configuration=Debug -Clean)
|
||||
|
||||
powershell -ExecutionPolicy Bypass -NoLogo -NoProfile -File "%~dp0\BuildSparsePackage.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
422
src/PackageIdentity/BuildSparsePackage.ps1
Normal file
422
src/PackageIdentity/BuildSparsePackage.ps1
Normal file
@@ -0,0 +1,422 @@
|
||||
#Requires -Version 5.1
|
||||
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("arm64", "x64")]
|
||||
[string]$Platform = "x64",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Debug", "Release")]
|
||||
[string]$Configuration = "Release",
|
||||
|
||||
[switch]$Clean,
|
||||
[switch]$ForceCert,
|
||||
[switch]$NoSign,
|
||||
[switch]$CIBuild
|
||||
)
|
||||
|
||||
# PowerToys sparse packaging helper.
|
||||
# Generates a sparse MSIX (no payload) that grants package identity to selected Win32 components.
|
||||
# Multiple applications (PowerOCR, Settings UI, etc.) can share this single sparse identity.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$isCIBuild = $false
|
||||
if ($CIBuild.IsPresent) {
|
||||
$isCIBuild = $true
|
||||
} elseif ($env:CIBuild) {
|
||||
$isCIBuild = $env:CIBuild -ieq 'true'
|
||||
}
|
||||
|
||||
$currentPublisherHint = $script:Config.CertSubject
|
||||
|
||||
# Configuration constants - centralized management
|
||||
$script:Config = @{
|
||||
IdentityName = "Microsoft.PowerToys.SparseApp"
|
||||
SparseMsixName = "PowerToysSparse.msix"
|
||||
CertPrefix = "PowerToysSparse"
|
||||
CertSubject = 'CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US'
|
||||
CertValidMonths = 12
|
||||
}
|
||||
|
||||
#region Helper Functions
|
||||
|
||||
function Find-WindowsSDKTool {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ToolName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Architecture = "x64"
|
||||
)
|
||||
|
||||
# Simple fallback: check common Windows SDK locations
|
||||
$commonPaths = @(
|
||||
"${env:ProgramFiles}\Windows Kits\10\bin\*\$Architecture\$ToolName",
|
||||
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\$Architecture\$ToolName",
|
||||
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x86\$ToolName" # SignTool fallback
|
||||
)
|
||||
|
||||
foreach ($pattern in $commonPaths) {
|
||||
$found = Get-ChildItem $pattern -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
Write-BuildLog "Found $ToolName at: $($found.FullName)" -Level Info
|
||||
return $found.FullName
|
||||
}
|
||||
}
|
||||
|
||||
throw "$ToolName not found. Please ensure Windows SDK is installed."
|
||||
}
|
||||
|
||||
function Test-CertificateValidity {
|
||||
param([string]$ThumbprintFile)
|
||||
|
||||
if (-not (Test-Path $ThumbprintFile)) { return $false }
|
||||
|
||||
try {
|
||||
$thumb = (Get-Content $ThumbprintFile -Raw).Trim()
|
||||
if (-not $thumb) { return $false }
|
||||
$cert = Get-Item "cert:\CurrentUser\My\$thumb" -ErrorAction Stop
|
||||
return $cert.HasPrivateKey -and $cert.NotAfter -gt (Get-Date)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Write-BuildLog {
|
||||
param([string]$Message, [string]$Level = "Info")
|
||||
|
||||
$colors = @{ Error = "Red"; Warning = "Yellow"; Success = "Green"; Info = "Cyan" }
|
||||
$color = if ($colors.ContainsKey($Level)) { $colors[$Level] } else { "White" }
|
||||
|
||||
Write-Host "[$(Get-Date -f 'HH:mm:ss')] $Message" -ForegroundColor $color
|
||||
}
|
||||
|
||||
function Stop-FileProcesses {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FilePath
|
||||
)
|
||||
|
||||
# This function is kept for compatibility but simplified since
|
||||
# the staging directory approach resolves the file lock issues
|
||||
Write-Verbose "File process check for: $FilePath"
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
# Environment diagnostics for troubleshooting
|
||||
Write-BuildLog "Starting PackageIdentity build process..." -Level Info
|
||||
Write-BuildLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
|
||||
try {
|
||||
$execPolicy = Get-ExecutionPolicy
|
||||
Write-BuildLog "Execution Policy: $execPolicy" -Level Info
|
||||
} catch {
|
||||
Write-BuildLog "Execution Policy: Unable to determine (MSBuild environment)" -Level Info
|
||||
}
|
||||
Write-BuildLog "Current User: $env:USERNAME" -Level Info
|
||||
Write-BuildLog "Build Platform: $Platform, Configuration: $Configuration" -Level Info
|
||||
|
||||
# Check for Visual Studio environment
|
||||
if ($env:VSINSTALLDIR) {
|
||||
Write-BuildLog "Running in Visual Studio environment: $env:VSINSTALLDIR" -Level Info
|
||||
}
|
||||
|
||||
# Ensure certificate provider is available
|
||||
try {
|
||||
# Force load certificate provider for MSBuild environment
|
||||
if (-not (Get-PSProvider -PSProvider Certificate -ErrorAction SilentlyContinue)) {
|
||||
Write-BuildLog "Loading certificate provider..." -Level Warning
|
||||
Import-Module Microsoft.PowerShell.Security -Force
|
||||
}
|
||||
if (-not (Test-Path 'Cert:\CurrentUser')) {
|
||||
Write-BuildLog "Certificate drive not available, attempting to initialize..." -Level Warning
|
||||
Import-Module PKI -ErrorAction SilentlyContinue
|
||||
# Try to access the certificate store to force initialization
|
||||
Get-ChildItem "Cert:\CurrentUser\My" -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
} catch {
|
||||
Write-BuildLog ("Note: Certificate provider setup may need manual configuration: {0}" -f $_) -Level Warning
|
||||
}
|
||||
|
||||
# Project root folder (now set to current script folder for local builds)
|
||||
$ProjectRoot = $PSScriptRoot
|
||||
$UserFolder = Join-Path $ProjectRoot '.user'
|
||||
if (-not (Test-Path $UserFolder)) { New-Item -ItemType Directory -Path $UserFolder | Out-Null }
|
||||
|
||||
# Certificate file paths using configuration
|
||||
$prefix = $script:Config.CertPrefix
|
||||
$CertThumbFile, $CertCerFile = @('.thumbprint', '.cer') |
|
||||
ForEach-Object { Join-Path $UserFolder "$prefix.certificate.sample$_" }
|
||||
|
||||
# Clean option: remove bin/obj and uninstall existing sparse package if present
|
||||
if ($Clean) {
|
||||
Write-BuildLog "Cleaning build artifacts..." -Level Info
|
||||
'bin','obj' | ForEach-Object {
|
||||
$target = Join-Path $ProjectRoot $_
|
||||
if (Test-Path $target) { Remove-Item $target -Recurse -Force }
|
||||
}
|
||||
Write-BuildLog "Attempting to remove existing sparse package (best effort)" -Level Info
|
||||
try { Get-AppxPackage -Name $script:Config.IdentityName | Remove-AppxPackage } catch {}
|
||||
}
|
||||
|
||||
# Force certificate regeneration if requested
|
||||
if ($ForceCert -and (Test-Path $UserFolder)) {
|
||||
Write-BuildLog "ForceCert specified: removing existing certificate artifacts..." -Level Warning
|
||||
Remove-Item $UserFolder -Recurse -Force
|
||||
New-Item -ItemType Directory -Path $UserFolder | Out-Null
|
||||
}
|
||||
|
||||
# Ensure dev cert (development only; not for production use) - skip if NoSign specified
|
||||
$needNewCert = -not $NoSign -and (-not (Test-Path $CertThumbFile) -or $ForceCert -or -not (Test-CertificateValidity -ThumbprintFile $CertThumbFile))
|
||||
|
||||
if ($needNewCert) {
|
||||
Write-BuildLog "Generating development certificate (prefix=$($script:Config.CertPrefix))..." -Level Info
|
||||
|
||||
# Clear stale files in the certificate cache
|
||||
if (Test-Path $UserFolder) {
|
||||
Get-ChildItem -Path $UserFolder | ForEach-Object {
|
||||
if ($_.PSIsContainer) {
|
||||
Remove-Item $_.FullName -Recurse -Force
|
||||
} else {
|
||||
Remove-Item $_.FullName -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path $UserFolder)) {
|
||||
New-Item -ItemType Directory -Path $UserFolder | Out-Null
|
||||
}
|
||||
|
||||
$now = Get-Date
|
||||
$expiration = $now.AddMonths($script:Config.CertValidMonths)
|
||||
# Subject MUST match <Identity Publisher="..."> inside AppxManifest.xml
|
||||
$friendlyName = "PowerToys Dev Sparse Cert Create=$now"
|
||||
$keyFriendly = "PowerToys Dev Sparse Key Create=$now"
|
||||
|
||||
$certStore = 'cert:\CurrentUser\My'
|
||||
$ekuOid = '2.5.29.37'
|
||||
$ekuValue = '1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13'
|
||||
$eku = "$ekuOid={text}$ekuValue"
|
||||
|
||||
$cert = New-SelfSignedCertificate -CertStoreLocation $certStore `
|
||||
-NotAfter $expiration `
|
||||
-Subject $script:Config.CertSubject `
|
||||
-FriendlyName $friendlyName `
|
||||
-KeyFriendlyName $keyFriendly `
|
||||
-KeyDescription $keyFriendly `
|
||||
-TextExtension $eku
|
||||
|
||||
# Export certificate files
|
||||
Set-Content -Path $CertThumbFile -Value $cert.Thumbprint -Force
|
||||
Export-Certificate -Cert $cert -FilePath $CertCerFile -Force | Out-Null
|
||||
}
|
||||
|
||||
# Determine output directory - using PowerToys standard structure
|
||||
# Navigate to PowerToys root (two levels up from src/PackageIdentity)
|
||||
$PowerToysRoot = Split-Path (Split-Path $ProjectRoot -Parent) -Parent
|
||||
$outDir = Join-Path $PowerToysRoot "$Platform\$Configuration"
|
||||
|
||||
if (-not (Test-Path $outDir)) {
|
||||
Write-BuildLog "Creating output directory: $outDir" -Level Info
|
||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# PackageIdentity folder (this script location) containing the sparse manifest and assets
|
||||
$sparseDir = $PSScriptRoot
|
||||
$manifestPath = Join-Path $sparseDir 'AppxManifest.xml'
|
||||
if (-not (Test-Path $manifestPath)) { throw "Missing AppxManifest.xml in PackageIdentity folder: $manifestPath" }
|
||||
|
||||
$versionPropsPath = Join-Path $PowerToysRoot 'src\Version.props'
|
||||
$targetManifestVersion = $null
|
||||
$versionCandidate = $null
|
||||
if (Test-Path $versionPropsPath) {
|
||||
try {
|
||||
[xml]$propsXml = Get-Content -Path $versionPropsPath -Raw
|
||||
$versionCandidate = $propsXml.Project.PropertyGroup.Version
|
||||
} catch {
|
||||
Write-BuildLog ("Unable to read version from {0}: {1}" -f $versionPropsPath, $_) -Level Warning
|
||||
}
|
||||
} else {
|
||||
Write-BuildLog "Version.props not found at $versionPropsPath; manifest version will remain unchanged." -Level Warning
|
||||
}
|
||||
|
||||
if ($versionCandidate) {
|
||||
$targetManifestVersion = $versionCandidate.Trim()
|
||||
if (($targetManifestVersion -split '\.').Count -lt 4) {
|
||||
$targetManifestVersion = "$targetManifestVersion.0"
|
||||
}
|
||||
Write-BuildLog "Using sparse package version from Version.props: $targetManifestVersion" -Level Info
|
||||
} else {
|
||||
Write-BuildLog "No version value provided; manifest version will remain unchanged." -Level Info
|
||||
}
|
||||
|
||||
# Find MakeAppx.exe from Windows SDK
|
||||
try {
|
||||
$hostSdkArchitecture = if ([System.Environment]::Is64BitProcess) { 'x64' } else { 'x86' }
|
||||
$makeAppxPath = Find-WindowsSDKTool -ToolName "makeappx.exe" -Architecture $hostSdkArchitecture
|
||||
} catch {
|
||||
Write-Error "MakeAppx.exe not found. Please ensure Windows SDK is installed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Pack sparse MSIX from PackageIdentity folder
|
||||
$msixPath = Join-Path $outDir $script:Config.SparseMsixName
|
||||
|
||||
# Clean up existing MSIX file
|
||||
if (Test-Path $msixPath) {
|
||||
Write-BuildLog "Removing existing MSIX file..." -Level Info
|
||||
try {
|
||||
Remove-Item $msixPath -Force -ErrorAction Stop
|
||||
Write-BuildLog "Successfully removed existing MSIX file" -Level Success
|
||||
} catch {
|
||||
Write-BuildLog ("Warning: Could not remove existing MSIX file: {0}" -f $_) -Level Warning
|
||||
}
|
||||
}
|
||||
|
||||
# Create a clean staging directory to avoid file lock issues
|
||||
$stagingDir = Join-Path $outDir "staging"
|
||||
if (Test-Path $stagingDir) {
|
||||
Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
Write-BuildLog "Creating clean staging directory for packaging..." -Level Info
|
||||
|
||||
# Copy only essential files to staging directory to avoid file locks
|
||||
$essentialFiles = @(
|
||||
"AppxManifest.xml"
|
||||
"Images\*"
|
||||
)
|
||||
|
||||
foreach ($filePattern in $essentialFiles) {
|
||||
$sourcePath = Join-Path $sparseDir $filePattern
|
||||
$relativePath = $filePattern
|
||||
|
||||
if ($filePattern.Contains('\')) {
|
||||
$targetDir = Join-Path $stagingDir (Split-Path $relativePath -Parent)
|
||||
if (-not (Test-Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
if ($filePattern.EndsWith('\*')) {
|
||||
# Copy directory contents
|
||||
$sourceDir = $sourcePath.TrimEnd('\*')
|
||||
$targetDir = Join-Path $stagingDir (Split-Path $relativePath.TrimEnd('\*') -Parent)
|
||||
if (Test-Path $sourceDir) {
|
||||
Copy-Item -Path "$sourceDir\*" -Destination $targetDir -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
# Copy single file
|
||||
$targetPath = Join-Path $stagingDir $relativePath
|
||||
if (Test-Path $sourcePath) {
|
||||
Copy-Item -Path $sourcePath -Destination $targetPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure publisher matches the dev certificate for local builds
|
||||
$manifestStagingPath = Join-Path $stagingDir 'AppxManifest.xml'
|
||||
$shouldUseDevPublisher = -not $isCIBuild
|
||||
if (Test-Path $manifestStagingPath) {
|
||||
try {
|
||||
[xml]$manifestXml = Get-Content -Path $manifestStagingPath -Raw
|
||||
$identityNode = $manifestXml.Package.Identity
|
||||
$manifestChanged = $false
|
||||
if ($identityNode) {
|
||||
$currentPublisherHint = $identityNode.Publisher
|
||||
}
|
||||
|
||||
if ($identityNode) {
|
||||
if ($targetManifestVersion -and $identityNode.Version -ne $targetManifestVersion) {
|
||||
Write-BuildLog "Updating manifest version to $targetManifestVersion" -Level Info
|
||||
$identityNode.SetAttribute('Version', $targetManifestVersion)
|
||||
$manifestChanged = $true
|
||||
}
|
||||
|
||||
if ($shouldUseDevPublisher -and $identityNode.Publisher -ne $script:Config.CertSubject) {
|
||||
Write-BuildLog "Updating manifest publisher for local build" -Level Warning
|
||||
$identityNode.SetAttribute('Publisher', $script:Config.CertSubject)
|
||||
$manifestChanged = $true
|
||||
}
|
||||
$currentPublisherHint = $identityNode.Publisher
|
||||
}
|
||||
|
||||
if ($manifestChanged) {
|
||||
$manifestXml.Save($manifestStagingPath)
|
||||
}
|
||||
} catch {
|
||||
Write-BuildLog ("Unable to adjust manifest metadata: {0}" -f $_) -Level Warning
|
||||
}
|
||||
}
|
||||
|
||||
Write-BuildLog "Staging directory prepared with essential files only" -Level Success
|
||||
|
||||
# Pack MSIX using staging directory
|
||||
Write-BuildLog "Packing sparse MSIX ($($script:Config.SparseMsixName)) from staging -> $msixPath" -Level Info
|
||||
|
||||
& $makeAppxPath pack /d $stagingDir /p $msixPath /nv /o
|
||||
|
||||
if ($LASTEXITCODE -eq 0 -and (Test-Path $msixPath)) {
|
||||
Write-BuildLog "MSIX packaging completed successfully" -Level Success
|
||||
} else {
|
||||
Write-BuildLog "MakeAppx failed with exit code $LASTEXITCODE" -Level Error
|
||||
exit 1
|
||||
}
|
||||
} finally {
|
||||
# Clean up staging directory
|
||||
if (Test-Path $stagingDir) {
|
||||
try {
|
||||
Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-BuildLog "Cleaned up staging directory" -Level Info
|
||||
} catch {
|
||||
Write-BuildLog ("Warning: Could not clean up staging directory: {0}" -f $_) -Level Warning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Sign package (skip if NoSign specified for CI scenarios)
|
||||
if ($NoSign) {
|
||||
Write-BuildLog "Skipping signing (NoSign specified for CI build)" -Level Warning
|
||||
} else {
|
||||
# Use certificate thumbprint for signing (safer, no password)
|
||||
$certThumbprint = (Get-Content -Path $CertThumbFile -Raw).Trim()
|
||||
try {
|
||||
$signToolPath = Find-WindowsSDKTool -ToolName "signtool.exe"
|
||||
} catch {
|
||||
Write-Error "SignTool.exe not found. Please ensure Windows SDK is installed."
|
||||
exit 1
|
||||
}
|
||||
Write-BuildLog "Signing sparse MSIX using cert thumbprint $certThumbprint..." -Level Info
|
||||
& $signToolPath sign /fd SHA256 /sha1 $certThumbprint $msixPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "SignTool failed (exit $LASTEXITCODE). Ensure the certificate is in CurrentUser\\My and try -ForceCert if needed."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
$publisherHintFile = Join-Path $UserFolder "$($script:Config.CertPrefix).publisher.txt"
|
||||
try {
|
||||
Set-Content -Path $publisherHintFile -Value $currentPublisherHint -Force -NoNewline
|
||||
} catch {
|
||||
Write-BuildLog ("Unable to write publisher hint: {0}" -f $_) -Level Warning
|
||||
}
|
||||
|
||||
Write-BuildLog "`nPackage created: $msixPath" -Level Success
|
||||
|
||||
if ($NoSign) {
|
||||
Write-BuildLog "UNSIGNED package created for CI build. Sign before deployment." -Level Warning
|
||||
} else {
|
||||
Write-BuildLog "Install the dev certificate (once): $CertCerFile" -Level Info
|
||||
Write-BuildLog "Identity Name: $($script:Config.IdentityName)" -Level Info
|
||||
}
|
||||
|
||||
Write-BuildLog "Register sparse package:" -Level Info
|
||||
Write-BuildLog " Add-AppxPackage -Path `"$msixPath`" -ExternalLocation `"$outDir`"" -Level Warning
|
||||
Write-BuildLog "(If already installed and you changed manifest only): Add-AppxPackage -Register `"$manifestPath`" -ExternalLocation `"$outDir`" -ForceApplicationShutdown" -Level Warning
|
||||
43
src/PackageIdentity/Check-ProcessIdentity.ps1
Normal file
43
src/PackageIdentity/Check-ProcessIdentity.ps1
Normal file
@@ -0,0 +1,43 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Determine whether a given process (by PID) runs with an MSIX/UWP package identity.
|
||||
.DESCRIPTION
|
||||
Calls the Windows API GetPackageFullName to check if the target process executes under an MSIX/Sparse App/UWP package identity.
|
||||
Returns the package full name when identity is present, or "No package identity" otherwise.
|
||||
.PARAMETER ProcessId
|
||||
The process ID to inspect.
|
||||
.EXAMPLE
|
||||
.\Check-ProcessIdentity.ps1 -pid 12345
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[int]$ProcessId
|
||||
)
|
||||
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
public class P {
|
||||
[DllImport("kernel32.dll", SetLastError=true)]
|
||||
public static extern IntPtr OpenProcess(uint a, bool b, int p);
|
||||
[DllImport("kernel32.dll", SetLastError=true)]
|
||||
public static extern bool CloseHandle(IntPtr h);
|
||||
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
||||
public static extern int GetPackageFullName(IntPtr h, ref int l, StringBuilder b);
|
||||
public static string G(int pid) {
|
||||
IntPtr h = OpenProcess(0x1000, false, pid);
|
||||
if (h == IntPtr.Zero) return "Failed to open process";
|
||||
int len = 0;
|
||||
GetPackageFullName(h, ref len, null);
|
||||
if (len == 0) { CloseHandle(h); return "No package identity"; }
|
||||
var sb = new StringBuilder(len);
|
||||
int r = GetPackageFullName(h, ref len, sb);
|
||||
CloseHandle(h);
|
||||
return r == 0 ? sb.ToString() : "Error:" + r;
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
$result = [P]::G($ProcessId)
|
||||
Write-Output $result
|
||||
BIN
src/PackageIdentity/Images/Square150x150Logo.png
Normal file
BIN
src/PackageIdentity/Images/Square150x150Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
BIN
src/PackageIdentity/Images/Square44x44Logo.png
Normal file
BIN
src/PackageIdentity/Images/Square44x44Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
BIN
src/PackageIdentity/Images/StoreLogo.png
Normal file
BIN
src/PackageIdentity/Images/StoreLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
120
src/PackageIdentity/PackageIdentity.vcxproj
Normal file
120
src/PackageIdentity/PackageIdentity.vcxproj
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- CI Build Configuration -->
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ForceCIPackaging>true</ForceCIPackaging>
|
||||
<NoSignCI>true</NoSignCI>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Target to generate sparse MSIX package -->
|
||||
<Target Name="GenerateSparsePackage" BeforeTargets="PrepareForBuild">
|
||||
<!-- Use NoSign only for CI builds to avoid certificate issues on hosted agents -->
|
||||
<PropertyGroup>
|
||||
<NoSignParam Condition="'$(NoSignCI)' == 'true'">-NoSign</NoSignParam>
|
||||
<NoSignParam Condition="'$(NoSignCI)' != 'true'"></NoSignParam>
|
||||
<CIBuildParam Condition="'$(CIBuild)' == 'true'">-CIBuild</CIBuildParam>
|
||||
<CIBuildParam Condition="'$(CIBuild)' != 'true'"></CIBuildParam>
|
||||
</PropertyGroup>
|
||||
|
||||
<Exec Command="pwsh -NonInteractive -ExecutionPolicy Bypass -File "$(MSBuildThisFileDirectory)BuildSparsePackage.ps1" -Platform $(Platform) -Configuration $(Configuration) $(NoSignParam) $(CIBuildParam)"
|
||||
ContinueOnError="false"
|
||||
WorkingDirectory="$(MSBuildThisFileDirectory)" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>15.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}</ProjectGuid>
|
||||
<RootNamespace>PackageIdentity</RootNamespace>
|
||||
<ProjectName>PackageIdentity</ProjectName>
|
||||
<UseFastUpToDateCheck>false</UseFastUpToDateCheck>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Utility</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Utility</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Utility</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Utility</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="..\Solution.props" />
|
||||
</ImportGroup>
|
||||
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="AppxManifest.xml" />
|
||||
<None Include="BuildSparsePackage.ps1" />
|
||||
<None Include="BuildSparsePackage.cmd" />
|
||||
<None Include="Check-ProcessIdentity.ps1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Image Include="Images\Square150x150Logo.png">
|
||||
<Filter>Images</Filter>
|
||||
</Image>
|
||||
<Image Include="Images\Square44x44Logo.png">
|
||||
<Filter>Images</Filter>
|
||||
</Image>
|
||||
<Image Include="Images\StoreLogo.png">
|
||||
<Filter>Images</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
|
||||
</Project>
|
||||
25
src/PackageIdentity/PackageIdentity.vcxproj.filters
Normal file
25
src/PackageIdentity/PackageIdentity.vcxproj.filters
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Images">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>png;jpg;jpeg;gif;bmp;ico</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="AppxManifest.xml" />
|
||||
<None Include="BuildSparsePackage.ps1" />
|
||||
<None Include="BuildSparsePackage.cmd" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="Images\Square150x150Logo.png">
|
||||
<Filter>Images</Filter>
|
||||
</Image>
|
||||
<Image Include="Images\Square44x44Logo.png">
|
||||
<Filter>Images</Filter>
|
||||
</Image>
|
||||
<Image Include="Images\StoreLogo.png">
|
||||
<Filter>Images</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
90
src/PackageIdentity/readme.md
Normal file
90
src/PackageIdentity/readme.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# PowerToys sparse package identity
|
||||
|
||||
This document describes how to build, sign, register, and consume the shared sparse MSIX package that grants package identity to select Win32 components of PowerToys.
|
||||
|
||||
## Package overview
|
||||
|
||||
The sparse package lives under `src/PackageIdentity`. It produces a payload-free MSIX whose `Identity` matches `Microsoft.PowerToys.SparseApp`. The manifest contains one entry per Win32 surface that should run with identity (for example Settings, PowerOCR, Image Resizer).
|
||||
|
||||
> The MSIX contains only metadata. When the package is registered you must point `-ExternalLocation` to the output folder that hosts the Win32 binaries (for example `x64\Release`).
|
||||
|
||||
## Building the sparse package locally
|
||||
|
||||
Two options are available:
|
||||
|
||||
- Build the utility project from Visual Studio: `PackageIdentity.vcxproj` defines a `GenerateSparsePackage` target that runs before `PrepareForBuild` and invokes the helper script automatically.
|
||||
- Invoke the helper script directly from PowerShell:
|
||||
|
||||
```powershell
|
||||
$repoRoot = "C:/git/PowerToys"
|
||||
pwsh "$repoRoot/src/PackageIdentity/BuildSparsePackage.ps1" -Platform x64 -Configuration Release
|
||||
```
|
||||
|
||||
Supported switches:
|
||||
|
||||
- `-Clean` removes previous `bin`/`obj` outputs and uninstalls existing installation.
|
||||
- `-ForceCert` regenerates the local dev certificate (.pfx/.cer/.pwd/.thumbprint) under `src/PackageIdentity/.user`.
|
||||
- `-NoSign` skips signing. The MSIX still builds but must be signed before deployment.
|
||||
- `-CIBuild` (or setting `$env:CIBuild = 'true'`) keeps the manifest publisher intact and skips the local cert substitution.
|
||||
|
||||
The script determines the proper `makeappx.exe` for the host build machine (x64 on typical developer boxes) and creates `PowerToysSparse.msix` in `{repo}\<Platform>\<Configuration>`.
|
||||
|
||||
> After packaging finishes, the helper also emits `src/PackageIdentity/.user/PowerToysSparse.publisher.txt`. This file mirrors the publisher string Windows will see once the sparse package is registered, which downstream projects can read to stay in sync when generating their own manifests.
|
||||
|
||||
## Local signing basics
|
||||
|
||||
When `-NoSign` is not used the script generates (or reuses) a development certificate and signs the package via `signtool.exe`:
|
||||
|
||||
1. Artifacts are stored in `src/PackageIdentity/.user/PowerToysSparse.certificate.sample.*` (`.cer` and `.thumbprint`).
|
||||
2. Install the `.cer` into `CurrentUser` → `TrustedPeople` (and `TrustedRoot`, if necessary) so Windows trusts the signature:
|
||||
|
||||
```powershell
|
||||
$repoRoot = "C:/git/PowerToys"
|
||||
Import-Certificate -FilePath "$repoRoot/src/PackageIdentity/.user/PowerToysSparse.certificate.sample.cer" -CertStoreLocation Cert:\CurrentUser\TrustedPeople
|
||||
```
|
||||
|
||||
3. The private key stays in the current user's personal certificate store.
|
||||
|
||||
## Registering or unregistering the package
|
||||
|
||||
After `PowerToysSparse.msix` is generated:
|
||||
|
||||
```powershell
|
||||
# First time registration
|
||||
$repoRoot = "C:/git/PowerToys"
|
||||
$outputRoot = Join-Path $repoRoot "x64/Release"
|
||||
Add-AppxPackage -Path (Join-Path $outputRoot "PowerToysSparse.msix") -ExternalLocation $outputRoot
|
||||
|
||||
# Re-register after manifest tweaks only
|
||||
Add-AppxPackage -Register (Join-Path $repoRoot "src/PackageIdentity/AppxManifest.xml") -ExternalLocation $outputRoot -ForceApplicationShutdown
|
||||
|
||||
# Remove the sparse identity
|
||||
Get-AppxPackage -Name Microsoft.PowerToys.SparseApp | Remove-AppxPackage
|
||||
```
|
||||
|
||||
`-ExternalLocation` should match the output folder that contains the Win32 executables declared in the manifest. Re-run registration whenever the manifest or executable layout changes.
|
||||
|
||||
## CI-specific guidance
|
||||
|
||||
- Pass `-CIBuild` to `BuildSparsePackage.ps1` (or build with `msbuild PackageIdentity.vcxproj /p:CIBuild=true`). This prevents the script from rewriting the manifest publisher to the local dev certificate subject.
|
||||
- The project automatically adds `-NoSign` only when `$(CIBuild)` is `true`. Local Debug and Release builds are signed with the development certificate.
|
||||
- Make sure the agent trusts whichever certificate signs the package. If the package remains unsigned (`-NoSign`) it cannot be installed on test machines until it is signed.
|
||||
|
||||
## Consuming the identity from other components
|
||||
|
||||
1. Add a new `<Application>` entry inside `src/PackageIdentity/AppxManifest.xml`. Use a unique `Id` (for example `PowerToys.MyModuleUI`) and set `Executable` to the Win32 binary relative to the `-ExternalLocation` root.
|
||||
2. Ensure the binary is copied into the platform/configuration output folder (`x64\Release`, `ARM64\Debug`, etc.) so the sparse package can locate it.
|
||||
3. Embed a sparse identity manifest in the Win32 binary so it binds to the MSIX identity at runtime. The manifest must declare an `<msix>` element with `packageName="Microsoft.PowerToys.SparseApp"`, `applicationId` matching the `<Application Id>`, and a `publisher` that matches the sparse package. Keep the manifest’s publisher in sync with `src/PackageIdentity/.user/PowerToysSparse.publisher.txt` (emitted by `BuildSparsePackage.ps1`). See `src/modules/imageresizer/ui/ImageResizerUI.csproj` for an example that points `ApplicationManifest` to `ImageResizerUI.dev.manifest` for local builds and switches to `ImageResizerUI.prod.manifest` when `$(CIBuild)` is `true`.
|
||||
4. Register or re-register the sparse package so Windows learns about the new application Id.
|
||||
5. To launch the Win32 surface with identity, use the `shell:AppsFolder` activation form (for example: `shell:AppsFolder\Microsoft.PowerToys.SparseApp_<PackageFamilyName>!PowerToys.MyModuleUI`) or activate it via `IApplicationActivationManager::ActivateApplication` using the same AppUserModelID.
|
||||
|
||||
- For locally built packages, resolve the `<PackageFamilyName>` with `Get-AppxPackage -Name Microsoft.PowerToys.SparseApp | Select-Object -ExpandProperty PackageFamilyName`.
|
||||
- Store-distributed builds use `Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe`. Local developer builds created by this script typically use a different family name derived from the dev certificate.
|
||||
|
||||
6. Context menu handlers or other launchers should fall back to the unpackaged executable path for environments where the sparse package is not present.
|
||||
|
||||
## Troubleshooting tips
|
||||
|
||||
- `Program 'makeappx.exe' failed to run`: make sure you are running an x64 PowerShell host. The script now chooses the appropriate makeappx automatically; update your repo if the log still points to an ARM64 binary.
|
||||
- `HRESULT 0x800B0109 (trust failure)`: install the development certificate into both `TrustedPeople` and `TrustedRoot` stores for the current user.
|
||||
- Stale registration: remove the package with `Remove-AppxPackage` and re-run the script with `-Clean` to rebuild from scratch.
|
||||
@@ -9,12 +9,6 @@
|
||||
<RootNamespace>CalculatorEngineCommon</RootNamespace>
|
||||
<AppxPackage>false</AppxPackage>
|
||||
</PropertyGroup>
|
||||
<!-- BEGIN common.build.pre.props -->
|
||||
<PropertyGroup Label="Configuration">
|
||||
<EnableHybridCRT>true</EnableHybridCRT>
|
||||
<UseCrtSDKReferenceStaticWarning Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReferenceStaticWarning>
|
||||
</PropertyGroup>
|
||||
<!-- END common.build.pre.props -->
|
||||
<!-- BEGIN cppwinrt.build.pre.props -->
|
||||
<PropertyGroup Label="Globals">
|
||||
<CppWinRTEnabled>true</CppWinRTEnabled>
|
||||
@@ -25,11 +19,9 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<MinimalCoreWin>true</MinimalCoreWin>
|
||||
<AppContainerApplication>true</AppContainerApplication>
|
||||
<AppContainerApplication>false</AppContainerApplication>
|
||||
<WindowsStoreApp>true</WindowsStoreApp>
|
||||
<ApplicationType>Windows Store</ApplicationType>
|
||||
<UseCrtSDKReference Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReference>
|
||||
<!-- The SDK reference breaks the Hybrid CRT -->
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- We have to use the Desktop platform for Hybrid CRT to work. -->
|
||||
@@ -148,43 +140,5 @@
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
<!-- BEGIN common.build.post.props -->
|
||||
<!--
|
||||
The Hybrid CRT model statically links the runtime and STL and dynamically
|
||||
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
|
||||
WinAppSDK asserts that this is "supported according to the CRT maintainer."
|
||||
|
||||
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
|
||||
-->
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- END common.build.post.props -->
|
||||
</Target> <!-- END common.build.post.props -->
|
||||
</Project>
|
||||
@@ -130,7 +130,7 @@ namespace ManagedCommon
|
||||
{
|
||||
exMessage +=
|
||||
"Inner exception: " + Environment.NewLine +
|
||||
ex.InnerException.GetType() + " (" + ex.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
|
||||
ex.InnerException.GetType() + " (" + ex.InnerException.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
|
||||
}
|
||||
|
||||
exMessage +=
|
||||
|
||||
@@ -63,14 +63,12 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -46,16 +46,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetName>notifications</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
<ClCompile>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">MultiThreadedDebug</RuntimeLibrary>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
|
||||
@@ -95,8 +93,6 @@
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">MultiThreaded</RuntimeLibrary>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar x:Name="titleBar">
|
||||
<TitleBar x:Name="titleBar" IsTabStop="False">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</ClCompile>
|
||||
@@ -49,7 +48,6 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar x:Name="titleBar">
|
||||
<TitleBar x:Name="titleBar" IsTabStop="False">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar x:Name="titleBar">
|
||||
<TitleBar x:Name="titleBar" IsTabStop="False">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
|
||||
@@ -189,7 +189,7 @@ bool SuperSonar<D>::Initialize(HINSTANCE hinst)
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD exStyle = WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle();
|
||||
DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle();
|
||||
HWND created = CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this);
|
||||
if (!created)
|
||||
{
|
||||
@@ -269,10 +269,6 @@ LRESULT SuperSonar<D>::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) n
|
||||
|
||||
case WM_NCHITTEST:
|
||||
return HTTRANSPARENT;
|
||||
|
||||
case WM_SETCURSOR:
|
||||
SetCursor(LoadCursor(nullptr, IDC_ARROW));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (message == WM_PRIV_SHORTCUT)
|
||||
@@ -539,7 +535,7 @@ void SuperSonar<D>::StartSonar()
|
||||
Trace::MousePointerFocused();
|
||||
// Cover the entire virtual screen.
|
||||
// HACK: Draw with 1 pixel off. Otherwise, Windows glitches the task bar transparency when a transparent window fill the whole screen.
|
||||
SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, SWP_NOACTIVATE);
|
||||
SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, 0);
|
||||
m_sonarPos = ptNowhere;
|
||||
OnMouseTimer();
|
||||
UpdateMouseSnooping();
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -82,7 +81,6 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -66,7 +65,6 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -66,7 +65,6 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -67,7 +66,6 @@
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -14,6 +14,9 @@ extern void InclusiveCrosshairsRequestUpdatePosition();
|
||||
extern void InclusiveCrosshairsEnsureOn();
|
||||
extern void InclusiveCrosshairsEnsureOff();
|
||||
extern void InclusiveCrosshairsSetExternalControl(bool enabled);
|
||||
extern void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation);
|
||||
extern bool InclusiveCrosshairsIsEnabled();
|
||||
extern void InclusiveCrosshairsSwitch();
|
||||
|
||||
// Non-Localizable strings
|
||||
namespace
|
||||
@@ -244,12 +247,19 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hotkeyId == 0)
|
||||
if (hotkeyId == 0) // Crosshairs activation
|
||||
{
|
||||
// If gliding cursor is active, cancel it and activate crosshairs
|
||||
if (m_glideState.load() != 0)
|
||||
{
|
||||
CancelGliding(true /*activateCrosshairs*/);
|
||||
return true;
|
||||
}
|
||||
// Otherwise, normal crosshairs toggle
|
||||
InclusiveCrosshairsSwitch();
|
||||
return true;
|
||||
}
|
||||
if (hotkeyId == 1)
|
||||
if (hotkeyId == 1) // Gliding cursor activation
|
||||
{
|
||||
HandleGlidingHotkey();
|
||||
return true;
|
||||
@@ -268,25 +278,44 @@ private:
|
||||
SendInput(2, inputs, sizeof(INPUT));
|
||||
}
|
||||
|
||||
// Cancel gliding without performing the final click (Escape handling)
|
||||
void CancelGliding()
|
||||
// Cancel gliding with option to activate crosshairs in user's preferred orientation
|
||||
void CancelGliding(bool activateCrosshairs)
|
||||
{
|
||||
int state = m_glideState.load();
|
||||
if (state == 0)
|
||||
{
|
||||
return; // nothing to cancel
|
||||
}
|
||||
|
||||
// Stop all gliding operations
|
||||
StopXTimer();
|
||||
StopYTimer();
|
||||
m_glideState = 0;
|
||||
InclusiveCrosshairsEnsureOff();
|
||||
UninstallKeyboardHook();
|
||||
|
||||
// Reset crosshairs control and restore user settings
|
||||
InclusiveCrosshairsSetExternalControl(false);
|
||||
InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
|
||||
|
||||
if (activateCrosshairs)
|
||||
{
|
||||
// User is switching to crosshairs mode - enable with their settings
|
||||
InclusiveCrosshairsEnsureOn();
|
||||
}
|
||||
else
|
||||
{
|
||||
// User canceled (Escape) - turn off crosshairs completely
|
||||
InclusiveCrosshairsEnsureOff();
|
||||
}
|
||||
|
||||
// Reset gliding state
|
||||
if (auto s = m_state)
|
||||
{
|
||||
s->xFraction = 0.0;
|
||||
s->yFraction = 0.0;
|
||||
}
|
||||
Logger::debug("Gliding cursor cancelled via Escape key");
|
||||
|
||||
Logger::debug("Gliding cursor cancelled (activateCrosshairs={})", activateCrosshairs ? 1 : 0);
|
||||
}
|
||||
|
||||
// Stateless helpers operating on shared State
|
||||
@@ -425,21 +454,22 @@ private:
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Simulate the AHK state machine
|
||||
|
||||
int state = m_glideState.load();
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
case 0: // Starting gliding
|
||||
{
|
||||
// For detect for cancel key
|
||||
// Install keyboard hook for Escape cancellation
|
||||
InstallKeyboardHook();
|
||||
// Ensure crosshairs on (do not toggle off if already on)
|
||||
InclusiveCrosshairsEnsureOn();
|
||||
// Disable internal mouse hook so we control position updates explicitly
|
||||
|
||||
// Force crosshairs visible in BOTH orientation for gliding, regardless of user setting
|
||||
// Set external control before enabling to prevent internal movement hook from attaching
|
||||
InclusiveCrosshairsSetExternalControl(true);
|
||||
// Override crosshairs to show both for Gliding Cursor
|
||||
InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both);
|
||||
InclusiveCrosshairsEnsureOn(); // Always ensure they are visible
|
||||
|
||||
// Initialize gliding state
|
||||
s->currentXPos = 0;
|
||||
s->currentXSpeed = s->fastHSpeed;
|
||||
s->xFraction = 0.0;
|
||||
@@ -447,20 +477,17 @@ private:
|
||||
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
|
||||
SetCursorPos(0, y);
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
|
||||
m_glideState = 1;
|
||||
StartXTimer();
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
// Slow horizontal
|
||||
case 1: // Slow horizontal
|
||||
s->currentXSpeed = s->slowHSpeed;
|
||||
m_glideState = 2;
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
case 2: // Switch to vertical fast
|
||||
{
|
||||
// Stop horizontal, start vertical (fast)
|
||||
StopXTimer();
|
||||
s->currentYSpeed = s->fastVSpeed;
|
||||
s->currentYPos = 0;
|
||||
@@ -471,33 +498,37 @@ private:
|
||||
StartYTimer();
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
// Slow vertical
|
||||
case 3: // Slow vertical
|
||||
s->currentYSpeed = s->slowVSpeed;
|
||||
m_glideState = 4;
|
||||
break;
|
||||
}
|
||||
case 4:
|
||||
case 4: // Finalize (click and end)
|
||||
default:
|
||||
{
|
||||
UninstallKeyboardHook();
|
||||
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
|
||||
// Complete the gliding sequence
|
||||
StopYTimer();
|
||||
m_glideState = 0;
|
||||
LeftClick();
|
||||
InclusiveCrosshairsEnsureOff();
|
||||
|
||||
// Restore normal crosshairs operation and turn them off
|
||||
InclusiveCrosshairsSetExternalControl(false);
|
||||
// Restore original crosshairs orientation setting
|
||||
InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
|
||||
s->xFraction = 0.0;
|
||||
s->yFraction = 0.0;
|
||||
InclusiveCrosshairsEnsureOff();
|
||||
|
||||
UninstallKeyboardHook();
|
||||
|
||||
// Reset state
|
||||
if (auto sp = m_state)
|
||||
{
|
||||
sp->xFraction = 0.0;
|
||||
sp->yFraction = 0.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Low-level keyboard hook procedures
|
||||
// Low-level keyboard hook for Escape cancellation
|
||||
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (nCode == HC_ACTION)
|
||||
@@ -509,14 +540,11 @@ private:
|
||||
{
|
||||
if (inst->m_enabled && inst->m_glideState.load() != 0)
|
||||
{
|
||||
inst->UninstallKeyboardHook();
|
||||
inst->CancelGliding();
|
||||
inst->CancelGliding(false); // Escape cancels without activating crosshairs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not swallow Escape; pass it through
|
||||
return CallNextHookEx(nullptr, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ namespace MouseWithoutBorders
|
||||
internal const int WM_RBUTTONDBLCLK = 0x206;
|
||||
internal const int WM_MBUTTONDBLCLK = 0x209;
|
||||
internal const int WM_MOUSEWHEEL = 0x020A;
|
||||
internal const int WM_MOUSEHWHEEL = 0x020E;
|
||||
|
||||
internal const int WM_KEYDOWN = 0x100;
|
||||
internal const int WM_KEYUP = 0x101;
|
||||
|
||||
@@ -204,6 +204,9 @@ namespace MouseWithoutBorders.Class
|
||||
case Common.WM_MOUSEWHEEL:
|
||||
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.WHEEL;
|
||||
break;
|
||||
case Common.WM_MOUSEHWHEEL:
|
||||
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.HWHEEL;
|
||||
break;
|
||||
case Common.WM_XBUTTONUP:
|
||||
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XUP;
|
||||
break;
|
||||
|
||||
@@ -556,6 +556,7 @@ namespace MouseWithoutBorders.Class
|
||||
XDOWN = 0x0080,
|
||||
XUP = 0x0100,
|
||||
WHEEL = 0x0800,
|
||||
HWHEEL = 0x1000,
|
||||
VIRTUALDESK = 0x4000,
|
||||
ABSOLUTE = 0x8000,
|
||||
}
|
||||
|
||||
@@ -67,8 +67,6 @@
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<ModuleDefinitionFile>dll.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>runtimeobject.lib;$(CoreLibraryDependencies)</AdditionalDependencies>
|
||||
<IgnoreSpecificDefaultLibraries>
|
||||
</IgnoreSpecificDefaultLibraries>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>del $(OutDir)\NewPlusPackage.msix /q
|
||||
@@ -100,8 +98,6 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<ModuleDefinitionFile>dll.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>runtimeobject.lib;$(CoreLibraryDependencies)</AdditionalDependencies>
|
||||
<IgnoreSpecificDefaultLibraries>
|
||||
</IgnoreSpecificDefaultLibraries>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>del $(OutDir)\NewPlusPackage.msix /q
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -40,7 +39,6 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <WorkspacesLib/trace.h>
|
||||
#include <WorkspacesLib/WorkspacesData.h>
|
||||
@@ -17,6 +18,9 @@
|
||||
|
||||
#include "resource.h"
|
||||
#include <common/utils/EventWaiter.h>
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <memory>
|
||||
|
||||
// Non-localizable
|
||||
const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe";
|
||||
@@ -69,6 +73,8 @@ public:
|
||||
return app_key.c_str();
|
||||
}
|
||||
|
||||
pt::cli::IModuleCommandProvider* command_provider() override;
|
||||
|
||||
virtual std::optional<HotkeyEx> GetHotkeyEx() override
|
||||
{
|
||||
return m_hotkey;
|
||||
@@ -359,9 +365,85 @@ private:
|
||||
.modifiersMask = MOD_CONTROL | MOD_WIN,
|
||||
.vkCode = 0xC0, // VK_OEM_3 key; usually `~
|
||||
};
|
||||
|
||||
std::unique_ptr<pt::cli::IModuleCommandProvider> m_cliProvider;
|
||||
|
||||
pt::cli::CommandResult HandleList(const json::JsonObject& args) const;
|
||||
};
|
||||
|
||||
class WorkspacesCommandProvider final : public pt::cli::IModuleCommandProvider
|
||||
{
|
||||
public:
|
||||
explicit WorkspacesCommandProvider(const WorkspacesModuleInterface& owner) :
|
||||
m_owner(owner)
|
||||
{
|
||||
}
|
||||
|
||||
std::wstring ModuleKey() const override
|
||||
{
|
||||
return L"workspaces";
|
||||
}
|
||||
|
||||
std::vector<pt::cli::CommandDescriptor> DescribeCommands() const override
|
||||
{
|
||||
pt::cli::CommandDescriptor listDescriptor;
|
||||
listDescriptor.action = L"list";
|
||||
listDescriptor.description = L"List configured workspaces.";
|
||||
return { std::move(listDescriptor) };
|
||||
}
|
||||
|
||||
pt::cli::CommandResult Execute(const pt::cli::CommandInvocation& invocation) override
|
||||
{
|
||||
std::wstring action = invocation.action;
|
||||
std::transform(action.begin(), action.end(), action.begin(), [](wchar_t ch) {
|
||||
return static_cast<wchar_t>(std::towlower(ch));
|
||||
});
|
||||
|
||||
if (action == L"list")
|
||||
{
|
||||
return m_owner.HandleList(invocation.args);
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Unsupported Workspaces command.");
|
||||
}
|
||||
|
||||
private:
|
||||
const WorkspacesModuleInterface& m_owner;
|
||||
};
|
||||
|
||||
pt::cli::IModuleCommandProvider* WorkspacesModuleInterface::command_provider()
|
||||
{
|
||||
if (!m_cliProvider)
|
||||
{
|
||||
m_cliProvider = std::make_unique<WorkspacesCommandProvider>(*this);
|
||||
}
|
||||
|
||||
return m_cliProvider.get();
|
||||
}
|
||||
|
||||
pt::cli::CommandResult WorkspacesModuleInterface::HandleList(const json::JsonObject& args) const
|
||||
{
|
||||
UNREFERENCED_PARAMETER(args);
|
||||
|
||||
json::JsonObject payload = json::JsonObject();
|
||||
auto workspacesPath = WorkspacesData::WorkspacesFile();
|
||||
payload.SetNamedValue(L"path", json::value(workspacesPath));
|
||||
|
||||
auto stored = json::from_file(workspacesPath);
|
||||
if (stored.has_value())
|
||||
{
|
||||
payload.SetNamedValue(L"data", json::value(*stored));
|
||||
}
|
||||
else
|
||||
{
|
||||
payload.SetNamedValue(L"data", json::value(json::JsonObject()));
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new WorkspacesModuleInterface();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -40,7 +39,6 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -40,7 +39,6 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<StringPooling>true</StringPooling>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
@@ -103,7 +102,6 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<StringPooling>true</StringPooling>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
@@ -126,7 +124,6 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x602;NDEBUG;_WIN32_WINNT=0x602;_WIN32_WINDOWS=0x501;WIN32;_WINDOWS;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<StringPooling>true</StringPooling>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
@@ -148,7 +145,6 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_M_IX86;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
@@ -169,7 +165,6 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_M_X64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
@@ -191,7 +186,6 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>__ZOOMIT_POWERTOYS__;_UNICODE;UNICODE;WINVER=0x0602;_DEBUG;_WIN32_WINNT=0x602.MSVC6;_WIN32_WINDOWS=0x600;WIN32;_WINDOWS;_WIN32_WINNT=0x602;MSVC6=1;_CRT_SECURE_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<ResourceCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;_M_ARM64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -40,7 +39,6 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -281,21 +281,7 @@ namespace Awake.Core
|
||||
TimeSpan remainingTime = expireAt - DateTimeOffset.Now;
|
||||
|
||||
Observable.Timer(remainingTime).Subscribe(
|
||||
_ =>
|
||||
{
|
||||
Logger.LogInfo("Completed expirable keep-awake.");
|
||||
CancelExistingThread();
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
SetPassiveKeepAwake();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("Exiting after expirable keep awake.");
|
||||
CompleteExit(Environment.ExitCode);
|
||||
}
|
||||
},
|
||||
_ => HandleTimerCompletion("expirable"),
|
||||
_tokenSource.Token);
|
||||
}
|
||||
|
||||
@@ -348,49 +334,46 @@ namespace Awake.Core
|
||||
|
||||
SetModeShellIcon();
|
||||
|
||||
ulong desiredDuration = (ulong)seconds * 1000;
|
||||
ulong targetDuration = Math.Min(desiredDuration, uint.MaxValue - 1) / 1000;
|
||||
var targetExpiryTime = DateTimeOffset.Now.AddSeconds(seconds);
|
||||
|
||||
if (desiredDuration > uint.MaxValue)
|
||||
{
|
||||
Logger.LogInfo($"The desired interval of {seconds} seconds ({desiredDuration}ms) exceeds the limit. Defaulting to maximum possible value: {targetDuration} seconds. Read more about existing limits in the official documentation: https://aka.ms/powertoys/awake");
|
||||
}
|
||||
|
||||
IObservable<long> timerObservable = Observable.Timer(TimeSpan.FromSeconds(targetDuration));
|
||||
IObservable<long> intervalObservable = Observable.Interval(TimeSpan.FromSeconds(1)).TakeUntil(timerObservable);
|
||||
IObservable<long> combinedObservable = Observable.CombineLatest(intervalObservable, timerObservable.StartWith(0), (elapsedSeconds, _) => elapsedSeconds + 1);
|
||||
|
||||
combinedObservable.Subscribe(
|
||||
elapsedSeconds =>
|
||||
{
|
||||
TimeRemaining = (uint)targetDuration - (uint)elapsedSeconds;
|
||||
if (TimeRemaining >= 0)
|
||||
Observable.Interval(TimeSpan.FromSeconds(1))
|
||||
.Select(_ => targetExpiryTime - DateTimeOffset.Now)
|
||||
.TakeWhile(remaining => remaining.TotalSeconds > 0)
|
||||
.Subscribe(
|
||||
remainingTimeSpan =>
|
||||
{
|
||||
TimeRemaining = (uint)remainingTimeSpan.TotalSeconds;
|
||||
|
||||
TrayHelper.SetShellIcon(
|
||||
TrayHelper.WindowHandle,
|
||||
$"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}][{TimeSpan.FromSeconds(TimeRemaining).ToHumanReadableString()}]",
|
||||
$"{Constants.FullAppName} [{Resources.AWAKE_TRAY_TEXT_TIMED}][{ScreenStateString}][{remainingTimeSpan.ToHumanReadableString()}]",
|
||||
TrayHelper.TimedIcon,
|
||||
TrayIconAction.Update);
|
||||
}
|
||||
},
|
||||
() =>
|
||||
{
|
||||
Logger.LogInfo("Completed timed thread.");
|
||||
CancelExistingThread();
|
||||
},
|
||||
_ => HandleTimerCompletion("timed"),
|
||||
_tokenSource.Token);
|
||||
}
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
// If we're using PowerToys settings, we need to make sure that
|
||||
// we just switch over the Passive Keep-Awake.
|
||||
SetPassiveKeepAwake();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("Exiting after timed keep-awake.");
|
||||
CompleteExit(Environment.ExitCode);
|
||||
}
|
||||
},
|
||||
_tokenSource.Token);
|
||||
/// <summary>
|
||||
/// Handles the common logic that should execute when a keep-awake timer completes. Resets
|
||||
/// the application state to Passive if configured; otherwise it exits.
|
||||
/// </summary>
|
||||
private static void HandleTimerCompletion(string timerType)
|
||||
{
|
||||
Logger.LogInfo($"Completed {timerType} keep-awake.");
|
||||
CancelExistingThread();
|
||||
|
||||
if (IsUsingPowerToysConfig)
|
||||
{
|
||||
// If running under PowerToys settings, just revert to the default Passive state.
|
||||
SetPassiveKeepAwake();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If running as a standalone process, exit cleanly.
|
||||
Logger.LogInfo($"Exiting after {timerType} keep-awake.");
|
||||
CompleteExit(Environment.ExitCode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,7 +13,13 @@
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/os-detect.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <cwctype>
|
||||
#include <optional>
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
|
||||
@@ -37,6 +43,105 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lp
|
||||
const static wchar_t* MODULE_NAME = L"Awake";
|
||||
const static wchar_t* MODULE_DESC = L"A module that keeps your computer awake on-demand.";
|
||||
|
||||
namespace
|
||||
{
|
||||
std::wstring to_lower_copy(std::wstring value)
|
||||
{
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](wchar_t ch) {
|
||||
return static_cast<wchar_t>(std::towlower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::wstring mode_to_string(int mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case 0:
|
||||
return L"passive";
|
||||
case 1:
|
||||
return L"indefinite";
|
||||
case 2:
|
||||
return L"timed";
|
||||
case 3:
|
||||
return L"expirable";
|
||||
default:
|
||||
return L"unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<uint32_t> parse_duration_string(const std::wstring& raw)
|
||||
{
|
||||
if (raw.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::wstring value = raw;
|
||||
double multiplier = 1.0;
|
||||
|
||||
wchar_t suffix = value.back();
|
||||
if (!iswdigit(suffix))
|
||||
{
|
||||
value.pop_back();
|
||||
if (suffix == L'h' || suffix == L'H')
|
||||
{
|
||||
multiplier = 60.0;
|
||||
}
|
||||
else if (suffix == L'm' || suffix == L'M')
|
||||
{
|
||||
multiplier = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
double numeric = std::stod(value);
|
||||
if (numeric < 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
double totalMinutes = numeric * multiplier;
|
||||
if (totalMinutes < 0 || totalMinutes > static_cast<double>(std::numeric_limits<uint32_t>::max()))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return static_cast<uint32_t>(totalMinutes);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<uint32_t> extract_duration_minutes(const json::JsonObject& args)
|
||||
{
|
||||
if (args.HasKey(L"durationMinutes"))
|
||||
{
|
||||
auto value = args.GetNamedNumber(L"durationMinutes");
|
||||
if (value < 0)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<uint32_t>(value);
|
||||
}
|
||||
|
||||
if (args.HasKey(L"duration"))
|
||||
{
|
||||
auto asString = args.GetNamedString(L"duration");
|
||||
return parse_duration_string(asString.c_str());
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
class Awake : public PowertoyModuleIface
|
||||
{
|
||||
std::wstring app_name;
|
||||
@@ -45,6 +150,7 @@ class Awake : public PowertoyModuleIface
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
PROCESS_INFORMATION p_info = {};
|
||||
std::unique_ptr<pt::cli::IModuleCommandProvider> m_cliProvider;
|
||||
|
||||
bool is_process_running()
|
||||
{
|
||||
@@ -176,9 +282,167 @@ public:
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
pt::cli::IModuleCommandProvider* command_provider() override;
|
||||
|
||||
pt::cli::CommandResult HandleStatus() const;
|
||||
pt::cli::CommandResult HandleSet(const json::JsonObject& args);
|
||||
};
|
||||
|
||||
class AwakeCommandProvider final : public pt::cli::IModuleCommandProvider
|
||||
{
|
||||
public:
|
||||
explicit AwakeCommandProvider(Awake& owner) :
|
||||
m_owner(owner)
|
||||
{
|
||||
}
|
||||
|
||||
std::wstring ModuleKey() const override
|
||||
{
|
||||
return L"awake";
|
||||
}
|
||||
|
||||
std::vector<pt::cli::CommandDescriptor> DescribeCommands() const override
|
||||
{
|
||||
std::vector<pt::cli::CommandParameter> setParameters{
|
||||
{ L"mode", false, L"Awake mode: passive | indefinite | timed." },
|
||||
{ L"durationMinutes", false, L"Total duration in minutes for timed mode." },
|
||||
{ L"duration", false, L"Duration with unit (e.g. 30m, 2h) for timed mode." },
|
||||
{ L"displayOn", false, L"Whether to keep the display active (true/false)." },
|
||||
};
|
||||
|
||||
pt::cli::CommandDescriptor setDescriptor;
|
||||
setDescriptor.action = L"set";
|
||||
setDescriptor.description = L"Configure the Awake module.";
|
||||
setDescriptor.parameters = std::move(setParameters);
|
||||
|
||||
pt::cli::CommandDescriptor statusDescriptor;
|
||||
statusDescriptor.action = L"status";
|
||||
statusDescriptor.description = L"Inspect the current Awake mode.";
|
||||
|
||||
return { std::move(setDescriptor), std::move(statusDescriptor) };
|
||||
}
|
||||
|
||||
pt::cli::CommandResult Execute(const pt::cli::CommandInvocation& invocation) override
|
||||
{
|
||||
auto action = to_lower_copy(invocation.action);
|
||||
if (action == L"set")
|
||||
{
|
||||
return m_owner.HandleSet(invocation.args);
|
||||
}
|
||||
|
||||
if (action == L"status")
|
||||
{
|
||||
return m_owner.HandleStatus();
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Unsupported Awake action.");
|
||||
}
|
||||
|
||||
private:
|
||||
Awake& m_owner;
|
||||
};
|
||||
|
||||
pt::cli::IModuleCommandProvider* Awake::command_provider()
|
||||
{
|
||||
if (!m_cliProvider)
|
||||
{
|
||||
m_cliProvider = std::make_unique<AwakeCommandProvider>(*this);
|
||||
}
|
||||
|
||||
return m_cliProvider.get();
|
||||
}
|
||||
|
||||
pt::cli::CommandResult Awake::HandleStatus() const
|
||||
{
|
||||
auto settings = PTSettingsHelper::load_module_settings(app_key);
|
||||
json::JsonObject payload = json::JsonObject();
|
||||
|
||||
if (!settings.HasKey(L"properties"))
|
||||
{
|
||||
payload.SetNamedValue(L"mode", json::value(L"unknown"));
|
||||
payload.SetNamedValue(L"keepDisplayOn", json::value(false));
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
auto properties = settings.GetNamedObject(L"properties");
|
||||
|
||||
const auto modeValue = static_cast<int>(properties.GetNamedNumber(L"mode", 0));
|
||||
payload.SetNamedValue(L"mode", json::value(mode_to_string(modeValue)));
|
||||
payload.SetNamedValue(L"modeValue", json::value(modeValue));
|
||||
payload.SetNamedValue(L"keepDisplayOn", json::value(properties.GetNamedBoolean(L"keepDisplayOn", false)));
|
||||
payload.SetNamedValue(L"intervalHours", json::value(static_cast<uint32_t>(properties.GetNamedNumber(L"intervalHours", 0))));
|
||||
payload.SetNamedValue(L"intervalMinutes", json::value(static_cast<uint32_t>(properties.GetNamedNumber(L"intervalMinutes", 0))));
|
||||
|
||||
if (properties.HasKey(L"expirationDateTime"))
|
||||
{
|
||||
payload.SetNamedValue(L"expirationDateTime", json::value(properties.GetNamedString(L"expirationDateTime")));
|
||||
}
|
||||
|
||||
return pt::cli::CommandResult::Success(std::move(payload));
|
||||
}
|
||||
|
||||
pt::cli::CommandResult Awake::HandleSet(const json::JsonObject& args)
|
||||
{
|
||||
std::wstring requestedMode = L"indefinite";
|
||||
if (args.HasKey(L"mode"))
|
||||
{
|
||||
requestedMode = to_lower_copy(std::wstring(args.GetNamedString(L"mode").c_str()));
|
||||
}
|
||||
|
||||
auto settings = PTSettingsHelper::load_module_settings(app_key);
|
||||
json::JsonObject properties = settings.HasKey(L"properties") ? settings.GetNamedObject(L"properties") : json::JsonObject();
|
||||
|
||||
const bool keepDisplayOn = args.GetNamedBoolean(L"displayOn", properties.GetNamedBoolean(L"keepDisplayOn", false));
|
||||
|
||||
int modeValue = 1; // default to indefinite
|
||||
if (requestedMode == L"passive")
|
||||
{
|
||||
modeValue = 0;
|
||||
}
|
||||
else if (requestedMode == L"indefinite" || requestedMode.empty())
|
||||
{
|
||||
modeValue = 1;
|
||||
}
|
||||
else if (requestedMode == L"timed")
|
||||
{
|
||||
modeValue = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_ARGS_INVALID", L"Unsupported mode. Use passive, indefinite, or timed.");
|
||||
}
|
||||
|
||||
properties.SetNamedValue(L"keepDisplayOn", json::value(keepDisplayOn));
|
||||
properties.SetNamedValue(L"mode", json::value(modeValue));
|
||||
|
||||
if (modeValue == 2)
|
||||
{
|
||||
auto durationMinutes = extract_duration_minutes(args);
|
||||
if (!durationMinutes.has_value() || durationMinutes.value() == 0)
|
||||
{
|
||||
return pt::cli::CommandResult::Error(L"E_ARGS_INVALID", L"Timed mode requires a non-zero duration.");
|
||||
}
|
||||
|
||||
const uint32_t totalMinutes = durationMinutes.value();
|
||||
const uint32_t hours = totalMinutes / 60;
|
||||
const uint32_t minutes = totalMinutes % 60;
|
||||
properties.SetNamedValue(L"intervalHours", json::value(hours));
|
||||
properties.SetNamedValue(L"intervalMinutes", json::value(minutes));
|
||||
}
|
||||
else
|
||||
{
|
||||
properties.SetNamedValue(L"intervalHours", json::value(0));
|
||||
properties.SetNamedValue(L"intervalMinutes", json::value(0));
|
||||
}
|
||||
|
||||
settings.SetNamedValue(L"properties", json::value(properties));
|
||||
PTSettingsHelper::save_module_settings(app_key, settings);
|
||||
|
||||
return HandleStatus();
|
||||
}
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new Awake();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,43 +141,4 @@
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
|
||||
<!-- BEGIN common.build.post.props -->
|
||||
<!--
|
||||
The Hybrid CRT model statically links the runtime and STL and dynamically
|
||||
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
|
||||
WinAppSDK asserts that this is "supported according to the CRT maintainer."
|
||||
|
||||
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
|
||||
-->
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- END common.build.post.props -->
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Storage.FileSystem;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
public static class PathHelper
|
||||
{
|
||||
public static bool Exists(string path, out bool isDirectory)
|
||||
{
|
||||
isDirectory = false;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? fullPath;
|
||||
try
|
||||
{
|
||||
fullPath = Path.GetFullPath(path);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = ExistsCore(fullPath, out isDirectory);
|
||||
if (result && IsDirectorySeparator(fullPath[^1]))
|
||||
{
|
||||
// Some sys-calls remove all trailing slashes and may give false positives for existing files.
|
||||
// We want to make sure that if the path ends in a trailing slash, it's truly a directory.
|
||||
return isDirectory;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize potential local/UNC file path text input: trim whitespace and surrounding quotes.
|
||||
/// Windows file paths cannot contain quotes, but user input can include them.
|
||||
/// </summary>
|
||||
public static string Unquote(string? text)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(text) ? (text ?? string.Empty) : text.Trim().Trim('"');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quick heuristic to determine if the string looks like a Windows file path (UNC or drive-letter based).
|
||||
/// </summary>
|
||||
public static bool LooksLikeFilePath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// UNC path
|
||||
if (path.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
// Win32 File Namespaces \\?\
|
||||
if (path.StartsWith(@"\\?\", StringComparison.Ordinal))
|
||||
{
|
||||
return IsSlow(path[4..]);
|
||||
}
|
||||
|
||||
// Basic UNC path validation: \\server\share or \\server\share\path
|
||||
var parts = path[2..].Split('\\', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return parts.Length >= 2; // At minimum: server and share
|
||||
}
|
||||
|
||||
// Drive letter path (e.g., C:\ or C:)
|
||||
return path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates path syntax without performing any I/O by using Path.GetFullPath.
|
||||
/// </summary>
|
||||
public static bool HasValidPathSyntax(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = Path.GetFullPath(path);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a string represents a valid Windows file path (local or network)
|
||||
/// using fast syntax validation only. Reuses LooksLikeFilePath and HasValidPathSyntax.
|
||||
/// </summary>
|
||||
public static bool IsValidFilePath(string? path)
|
||||
{
|
||||
return LooksLikeFilePath(path) && HasValidPathSyntax(path);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDirectorySeparator(char c)
|
||||
{
|
||||
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
|
||||
}
|
||||
|
||||
private static bool ExistsCore(string fullPath, out bool isDirectory)
|
||||
{
|
||||
var attributes = PInvoke.GetFileAttributes(fullPath);
|
||||
var result = attributes != PInvoke.INVALID_FILE_ATTRIBUTES;
|
||||
isDirectory = result && (attributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool IsSlow(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var root = Path.GetPathRoot(path);
|
||||
if (!string.IsNullOrEmpty(root))
|
||||
{
|
||||
if (root.Length > 2 && char.IsLetter(root[0]) && root[1] == ':')
|
||||
{
|
||||
return new DriveInfo(root).DriveType is not (DriveType.Fixed or DriveType.Ram);
|
||||
}
|
||||
else if (root.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
return !root.StartsWith(@"\\?\", StringComparison.Ordinal) || IsSlow(root[4..]);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,8 @@ MonitorFromWindow
|
||||
|
||||
SHOW_WINDOW_CMD
|
||||
ShellExecuteEx
|
||||
SEE_MASK_INVOKEIDLIST
|
||||
SEE_MASK_INVOKEIDLIST
|
||||
|
||||
GetFileAttributes
|
||||
FILE_FLAGS_AND_ATTRIBUTES
|
||||
INVALID_FILE_ATTRIBUTES
|
||||
@@ -306,6 +306,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
Command.PropertyChanged -= Command_PropertyChanged;
|
||||
Command = new(model.Command, PageContext);
|
||||
Command.InitializeProperties();
|
||||
Command.PropertyChanged += Command_PropertyChanged;
|
||||
|
||||
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
||||
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
||||
|
||||
@@ -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 CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
@@ -11,6 +13,13 @@ public partial class DetailsLinkViewModel(
|
||||
IDetailsElement _detailsElement,
|
||||
WeakReference<IPageContext> context) : DetailsElementViewModel(_detailsElement, context)
|
||||
{
|
||||
private static readonly string[] _initProperties = [
|
||||
nameof(Text),
|
||||
nameof(Link),
|
||||
nameof(IsLink),
|
||||
nameof(IsText),
|
||||
nameof(NavigateCommand)];
|
||||
|
||||
private readonly ExtensionObject<IDetailsLink> _dataModel =
|
||||
new(_detailsElement.Data as IDetailsLink);
|
||||
|
||||
@@ -22,6 +31,8 @@ public partial class DetailsLinkViewModel(
|
||||
|
||||
public bool IsText => !IsLink;
|
||||
|
||||
public RelayCommand? NavigateCommand { get; private set; }
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
base.InitializeProperties();
|
||||
@@ -38,9 +49,18 @@ public partial class DetailsLinkViewModel(
|
||||
Text = Link.ToString();
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(Text));
|
||||
UpdateProperty(nameof(Link));
|
||||
UpdateProperty(nameof(IsLink));
|
||||
UpdateProperty(nameof(IsText));
|
||||
if (Link is not null)
|
||||
{
|
||||
// Custom command to open a link in the default browser or app,
|
||||
// depending on the link type.
|
||||
// Binding Link to a Hyperlink(Button).NavigateUri works only for
|
||||
// certain URI schemes (e.g., http, https) and cannot open file:
|
||||
// scheme URIs or local files.
|
||||
NavigateCommand = new RelayCommand(
|
||||
() => ShellHelpers.OpenInShell(Link.ToString()),
|
||||
() => Link is not null);
|
||||
}
|
||||
|
||||
UpdateProperty(_initProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,17 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
EmptyContent = new(new(null), PageContext);
|
||||
}
|
||||
|
||||
private void FiltersPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(FiltersViewModel.Filters))
|
||||
{
|
||||
var filtersViewModel = sender as FiltersViewModel;
|
||||
var hasFilters = filtersViewModel?.Filters.Length > 0;
|
||||
HasFilters = hasFilters;
|
||||
UpdateProperty(nameof(HasFilters));
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -586,8 +597,11 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
EmptyContent = new(new(model.EmptyContent), PageContext);
|
||||
EmptyContent.SlowInitializeProperties();
|
||||
|
||||
Filters?.PropertyChanged -= FiltersPropertyChanged;
|
||||
Filters = new(new(model.Filters), PageContext);
|
||||
Filters.InitializeProperties();
|
||||
Filters?.PropertyChanged += FiltersPropertyChanged;
|
||||
|
||||
Filters?.InitializeProperties();
|
||||
UpdateProperty(nameof(Filters));
|
||||
|
||||
FetchItems();
|
||||
@@ -686,8 +700,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
EmptyContent.SlowInitializeProperties();
|
||||
break;
|
||||
case nameof(Filters):
|
||||
Filters?.PropertyChanged -= FiltersPropertyChanged;
|
||||
Filters = new(new(model.Filters), PageContext);
|
||||
Filters.InitializeProperties();
|
||||
Filters?.PropertyChanged += FiltersPropertyChanged;
|
||||
Filters?.InitializeProperties();
|
||||
break;
|
||||
case nameof(IsLoading):
|
||||
UpdateEmptyContent();
|
||||
@@ -757,6 +773,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
FilteredItems.Clear();
|
||||
}
|
||||
|
||||
Filters?.PropertyChanged -= FiltersPropertyChanged;
|
||||
Filters?.SafeCleanup();
|
||||
|
||||
var model = _model.Unsafe;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Used to navigate down one page in the page when pressing the PageDown key in the SearchBox.
|
||||
/// </summary>
|
||||
public record NavigatePageDownCommand
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Used to navigate up one page in the page when pressing the PageUp key in the SearchBox.
|
||||
/// </summary>
|
||||
public record NavigatePageUpCommand
|
||||
{
|
||||
}
|
||||
@@ -68,7 +68,9 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
// `IsLoading` property as a combo of this value and `IsInitialized`
|
||||
public bool ModelIsLoading { get; protected set; } = true;
|
||||
|
||||
public bool HasSearchBox { get; protected set; }
|
||||
public bool HasSearchBox { get; protected set; } = true;
|
||||
|
||||
public bool HasFilters { get; protected set; }
|
||||
|
||||
public IconInfoViewModel Icon { get; protected set; }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
@@ -48,6 +49,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
var oldValue = _currentPage;
|
||||
if (SetProperty(ref _currentPage, value))
|
||||
{
|
||||
oldValue.PropertyChanged -= CurrentPage_PropertyChanged;
|
||||
value.PropertyChanged += CurrentPage_PropertyChanged;
|
||||
|
||||
if (oldValue is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
@@ -63,6 +67,14 @@ public partial class ShellViewModel : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
private void CurrentPage_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(PageViewModel.HasSearchBox))
|
||||
{
|
||||
IsSearchBoxVisible = CurrentPage.HasSearchBox;
|
||||
}
|
||||
}
|
||||
|
||||
private IPage? _rootPage;
|
||||
|
||||
private bool _isNested;
|
||||
@@ -265,6 +277,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// Clear command bar, ViewModel initialization can already set new commands if it wants to
|
||||
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
|
||||
|
||||
// Kick off async loading of our ViewModel
|
||||
LoadPageViewModelAsync(pageViewModel, navigationToken)
|
||||
.ContinueWith(
|
||||
@@ -275,9 +290,6 @@ public partial class ShellViewModel : ObservableObject,
|
||||
{
|
||||
newCts.Dispose();
|
||||
}
|
||||
|
||||
// When we're done loading the page, then update the command bar to match
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
},
|
||||
navigationToken,
|
||||
TaskContinuationOptions.None,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project>
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -52,7 +52,7 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error building card from template: {Message}", ex.Message);
|
||||
Logger.LogError("Error building card from template", ex);
|
||||
error = ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ReloadFinishedMessage();
|
||||
@@ -410,5 +410,23 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
return ResourceManager.GetString("builtin_reload_subtitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} extensions found.
|
||||
/// </summary>
|
||||
public static string builtin_settings_extension_n_extensions_found {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_settings_extension_n_extensions_found", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} extensions installed.
|
||||
/// </summary>
|
||||
public static string builtin_settings_extension_n_extensions_installed {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_settings_extension_n_extensions_installed", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,4 +236,10 @@
|
||||
<data name="builtin_home_name" xml:space="preserve">
|
||||
<value>Home</value>
|
||||
</data>
|
||||
<data name="builtin_settings_extension_n_extensions_found" xml:space="preserve">
|
||||
<value>{0} extensions found</value>
|
||||
</data>
|
||||
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
|
||||
<value>{0} extensions installed</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Provides filtering over the list of provider settings view models.
|
||||
/// Intended to be used by the UI to bind a TextBox (SearchText) and an ItemsRepeater (FilteredProviders).
|
||||
/// </summary>
|
||||
public partial class SettingsExtensionsViewModel : ObservableObject
|
||||
{
|
||||
private static readonly CompositeFormat LabelNumberExtensionFound
|
||||
= CompositeFormat.Parse(Properties.Resources.builtin_settings_extension_n_extensions_found!);
|
||||
|
||||
private static readonly CompositeFormat LabelNumberExtensionInstalled
|
||||
= CompositeFormat.Parse(Properties.Resources.builtin_settings_extension_n_extensions_installed!);
|
||||
|
||||
private readonly ObservableCollection<ProviderSettingsViewModel> _source;
|
||||
private readonly TaskScheduler _uiScheduler;
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> FilteredProviders { get; } = [];
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
public string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
if (_searchText != value)
|
||||
{
|
||||
_searchText = value;
|
||||
OnPropertyChanged();
|
||||
ApplyFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ItemCounterText
|
||||
{
|
||||
get
|
||||
{
|
||||
var hasQuery = !string.IsNullOrWhiteSpace(_searchText);
|
||||
var count = hasQuery ? FilteredProviders.Count : _source.Count;
|
||||
var format = hasQuery ? LabelNumberExtensionFound : LabelNumberExtensionInstalled;
|
||||
return string.Format(CultureInfo.CurrentCulture, format, count);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowManualReloadOverlay
|
||||
{
|
||||
get;
|
||||
private set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowNoResultsPanel => !string.IsNullOrWhiteSpace(_searchText) && FilteredProviders.Count == 0;
|
||||
|
||||
public bool HasResults => !ShowNoResultsPanel;
|
||||
|
||||
public IRelayCommand ReloadExtensionsCommand { get; }
|
||||
|
||||
public SettingsExtensionsViewModel(ObservableCollection<ProviderSettingsViewModel> source, TaskScheduler uiScheduler)
|
||||
{
|
||||
_source = source;
|
||||
_uiScheduler = uiScheduler;
|
||||
_source.CollectionChanged += Source_CollectionChanged;
|
||||
ApplyFilter();
|
||||
|
||||
ReloadExtensionsCommand = new RelayCommand(ReloadExtensions);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ReloadFinishedMessage>(this, (_, _) =>
|
||||
{
|
||||
Task.Factory.StartNew(() => ShowManualReloadOverlay = false, CancellationToken.None, TaskCreationOptions.None, _uiScheduler);
|
||||
});
|
||||
}
|
||||
|
||||
private void Source_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
var query = _searchText;
|
||||
var filtered = ListHelpers.FilterList(_source, query, Matches);
|
||||
ListHelpers.InPlaceUpdateList(FilteredProviders, filtered);
|
||||
OnPropertyChanged(nameof(ItemCounterText));
|
||||
OnPropertyChanged(nameof(HasResults));
|
||||
OnPropertyChanged(nameof(ShowNoResultsPanel));
|
||||
}
|
||||
|
||||
private static int Matches(string query, ProviderSettingsViewModel item)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
return Contains(item.DisplayName, query)
|
||||
|| Contains(item.ExtensionName, query)
|
||||
|| Contains(item.ExtensionSubtext, query)
|
||||
? 100
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static bool Contains(string? haystack, string needle)
|
||||
{
|
||||
return !string.IsNullOrEmpty(haystack) && haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenStoreWithExtension(string? query)
|
||||
{
|
||||
const string extensionsAssocUri = "ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette";
|
||||
ShellHelpers.OpenInShell(extensionsAssocUri);
|
||||
}
|
||||
|
||||
private void ReloadExtensions()
|
||||
{
|
||||
ShowManualReloadOverlay = true;
|
||||
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>();
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public bool DisableAnimations { get; set; } = true;
|
||||
|
||||
public WindowPosition? LastWindowPosition { get; set; }
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -191,6 +193,7 @@ public partial class SettingsModel : ObservableObject
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(HistoryItem))]
|
||||
[JsonSerializable(typeof(SettingsModel))]
|
||||
[JsonSerializable(typeof(WindowPosition))]
|
||||
[JsonSerializable(typeof(AppStateModel))]
|
||||
[JsonSerializable(typeof(RecentCommandsManager))]
|
||||
[JsonSerializable(typeof(List<string>), TypeInfoPropertyName = "StringList")]
|
||||
@@ -208,4 +211,5 @@ public enum MonitorBehavior
|
||||
ToPrimary = 1,
|
||||
ToFocusedWindow = 2,
|
||||
InPlace = 3,
|
||||
ToLast = 4,
|
||||
}
|
||||
|
||||
@@ -140,6 +140,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
|
||||
|
||||
public SettingsExtensionsViewModel Extensions { get; }
|
||||
|
||||
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
|
||||
{
|
||||
_settings = settings;
|
||||
@@ -155,6 +157,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider);
|
||||
CommandProviders.Add(settingsModel);
|
||||
}
|
||||
|
||||
Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler);
|
||||
}
|
||||
|
||||
private IEnumerable<CommandProviderWrapper> GetCommandProviders()
|
||||
|
||||
@@ -259,6 +259,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
IsLoading = false;
|
||||
|
||||
// Send on the current thread; receivers should marshal to UI if needed
|
||||
WeakReferenceMessenger.Default.Send<ReloadFinishedMessage>();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
{
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(e.PropertyName));
|
||||
|
||||
if (e.PropertyName == "IsInitialized")
|
||||
if (e.PropertyName is "IsInitialized" or nameof(CommandItemViewModel.Command))
|
||||
{
|
||||
GenerateId();
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed class WindowPosition
|
||||
{
|
||||
public int X { get; set; }
|
||||
|
||||
public int Y { get; set; }
|
||||
|
||||
public int Width { get; set; }
|
||||
|
||||
public int Height { get; set; }
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
x:Class="Microsoft.CmdPal.UI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
@@ -15,11 +16,12 @@
|
||||
<ResourceDictionary Source="Styles/Settings.xaml" />
|
||||
<ResourceDictionary Source="Controls/Tag.xaml" />
|
||||
<ResourceDictionary Source="Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="Controls/IsEnabledTextBlock.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
|
||||
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="controls:CheckBoxWithDescriptionControl" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
100
src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.dark.svg
Normal file
100
src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.dark.svg
Normal file
@@ -0,0 +1,100 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_125_2801)">
|
||||
<g clip-path="url(#clip1_125_2801)">
|
||||
<path d="M0 5C0 4.44771 0.447715 4 1 4H22C22.5523 4 23 4.44772 23 5V18.5C23 20.9853 20.9853 23 18.5 23H4.5C2.01472 23 0 20.9853 0 18.5V5Z" fill="url(#paint0_linear_125_2801)"/>
|
||||
<mask id="mask0_125_2801" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="4" width="23" height="19">
|
||||
<path d="M0 5C0 4.44771 0.447715 4 1 4H22C22.5523 4 23 4.44772 23 5V18.5C23 20.9853 20.9853 23 18.5 23H4.5C2.01472 23 0 20.9853 0 18.5V5Z" fill="url(#paint1_linear_125_2801)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_125_2801)">
|
||||
<g filter="url(#filter0_dd_125_2801)">
|
||||
<path d="M6 5V1H17V5" stroke="url(#paint2_linear_125_2801)" stroke-width="2" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="6" y="8" width="11" height="11" rx="1" fill="white" fill-opacity="0.5"/>
|
||||
<g filter="url(#filter1_d_125_2801)">
|
||||
<path d="M11 9H7V13H11V9Z" fill="#F25022"/>
|
||||
<path d="M16 9H12V13H16V9Z" fill="#7FBA00"/>
|
||||
<path d="M16 14H12V18H16V14Z" fill="#FFB900"/>
|
||||
<path d="M11 14H7V18H11V14Z" fill="#00A4EF"/>
|
||||
</g>
|
||||
<path d="M6 5V2C6 1.44772 6.44772 1 7 1H16C16.5523 1 17 1.44772 17 2V5" stroke="url(#paint3_linear_125_2801)" stroke-width="2" stroke-linecap="round"/>
|
||||
<rect x="7" width="9" height="2" fill="url(#paint4_linear_125_2801)"/>
|
||||
<mask id="mask1_125_2801" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="7" y="0" width="9" height="2">
|
||||
<rect x="7" width="9" height="2" fill="url(#paint5_linear_125_2801)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_125_2801)">
|
||||
<g filter="url(#filter2_dd_125_2801)">
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_125_2801" x="3.5" y="-1" width="16" height="9" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2801"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_125_2801" result="effect2_dropShadow_125_2801"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2801" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_125_2801" x="6" y="8.25" width="11" height="11" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2801"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_125_2801" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_dd_125_2801" x="3.5" y="-2.5" width="16" height="8" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2801"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_125_2801" result="effect2_dropShadow_125_2801"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2801" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_125_2801" x1="5.22727" y1="1.19318" x2="9.5894" y2="22.315" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.270833" stop-color="white"/>
|
||||
<stop offset="1" stop-color="#DFDFDF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_125_2801" x1="11.5" y1="4" x2="15.4941" y2="23.743" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0078D4"/>
|
||||
<stop offset="1" stop-color="#114A8B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_125_2801" x1="11.5" y1="1" x2="11.8823" y2="5.29249" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#28AFEA"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_125_2801" x1="11.5" y1="1" x2="11.7158" y2="4.23049" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#30DAFF"/>
|
||||
<stop offset="1" stop-color="#0094D4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_125_2801" x1="11" y1="5.23303e-08" x2="11" y2="2" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#22BCFF"/>
|
||||
<stop offset="1" stop-color="#0088F0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_125_2801" x1="11" y1="5.23303e-08" x2="11" y2="2" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#28AFEA"/>
|
||||
<stop offset="1" stop-color="#3CCBF4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_125_2801">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_125_2801">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,100 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_125_2875)">
|
||||
<g clip-path="url(#clip1_125_2875)">
|
||||
<path d="M0 5C0 4.44771 0.447715 4 1 4H22C22.5523 4 23 4.44772 23 5V18.5C23 20.9853 20.9853 23 18.5 23H4.5C2.01472 23 0 20.9853 0 18.5V5Z" fill="url(#paint0_linear_125_2875)"/>
|
||||
<mask id="mask0_125_2875" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="4" width="23" height="19">
|
||||
<path d="M0 5C0 4.44771 0.447715 4 1 4H22C22.5523 4 23 4.44772 23 5V18.5C23 20.9853 20.9853 23 18.5 23H4.5C2.01472 23 0 20.9853 0 18.5V5Z" fill="url(#paint1_linear_125_2875)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_125_2875)">
|
||||
<g filter="url(#filter0_dd_125_2875)">
|
||||
<path d="M6 5V1H17V5" stroke="url(#paint2_linear_125_2875)" stroke-width="2" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="6" y="8" width="11" height="11" rx="1" fill="#243A5F" fill-opacity="0.5"/>
|
||||
<g filter="url(#filter1_d_125_2875)">
|
||||
<path d="M11 9H7V13H11V9Z" fill="#F25022"/>
|
||||
<path d="M16 9H12V13H16V9Z" fill="#7FBA00"/>
|
||||
<path d="M16 14H12V18H16V14Z" fill="#FFB900"/>
|
||||
<path d="M11 14H7V18H11V14Z" fill="#00A4EF"/>
|
||||
</g>
|
||||
<path d="M6 5V2C6 1.44772 6.44772 1 7 1H16C16.5523 1 17 1.44772 17 2V5" stroke="url(#paint3_linear_125_2875)" stroke-width="2" stroke-linecap="round"/>
|
||||
<rect x="7" width="9" height="2" fill="url(#paint4_linear_125_2875)"/>
|
||||
<mask id="mask1_125_2875" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="7" y="0" width="9" height="2">
|
||||
<rect x="7" width="9" height="2" fill="url(#paint5_linear_125_2875)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_125_2875)">
|
||||
<g filter="url(#filter2_dd_125_2875)">
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_125_2875" x="3.5" y="-1" width="16" height="9" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2875"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_125_2875" result="effect2_dropShadow_125_2875"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2875" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_125_2875" x="6" y="8.25" width="11" height="11" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2875"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_125_2875" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_dd_125_2875" x="3.5" y="-2.5" width="16" height="8" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2875"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_125_2875" result="effect2_dropShadow_125_2875"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2875" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_125_2875" x1="11.5" y1="4" x2="15.4941" y2="23.743" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0669BC"/>
|
||||
<stop offset="1" stop-color="#243A5F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_125_2875" x1="11.5" y1="4" x2="15.4941" y2="23.743" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0078D4"/>
|
||||
<stop offset="1" stop-color="#114A8B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_125_2875" x1="11.5" y1="1" x2="11.8823" y2="5.29249" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#28AFEA"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_125_2875" x1="11.5" y1="1" x2="11.7158" y2="4.23049" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#30DAFF"/>
|
||||
<stop offset="1" stop-color="#0094D4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_125_2875" x1="11" y1="5.23303e-08" x2="11" y2="2" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#22BCFF"/>
|
||||
<stop offset="1" stop-color="#0088F0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_125_2875" x1="11" y1="5.23303e-08" x2="11" y2="2" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#28AFEA"/>
|
||||
<stop offset="1" stop-color="#3CCBF4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_125_2875">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_125_2875">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
97
src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/WinGetLogo.svg
Normal file
97
src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/WinGetLogo.svg
Normal file
@@ -0,0 +1,97 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_125_2968)">
|
||||
<path d="M3 1.5C3 0.671573 3.67157 0 4.5 0H19.5C20.3284 0 21 0.671573 21 1.5V16.5C21 17.3284 20.3284 18 19.5 18H4.5C3.67157 18 3 17.3284 3 16.5V1.5Z" fill="#9C640A"/>
|
||||
<mask id="mask0_125_2968" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="0" width="18" height="18">
|
||||
<path d="M3 1.5C3 0.671573 3.67157 0 4.5 0H19.5C20.3284 0 21 0.671573 21 1.5V16.5C21 17.3284 20.3284 18 19.5 18H4.5C3.67157 18 3 17.3284 3 16.5V1.5Z" fill="#9C640A"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_125_2968)">
|
||||
<g filter="url(#filter0_dd_125_2968)">
|
||||
<path d="M1.5 4.5C1.5 3.67157 2.17157 3 3 3H21C21.8284 3 22.5 3.67157 22.5 4.5V19.5C22.5 20.3284 21.8284 21 21 21H3C2.17157 21 1.5 20.3284 1.5 19.5V4.5Z" fill="#BC822A"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M1.5 4.5C1.5 3.67157 2.17157 3 3 3H21C21.8284 3 22.5 3.67157 22.5 4.5V19.5C22.5 20.3284 21.8284 21 21 21H3C2.17157 21 1.5 20.3284 1.5 19.5V4.5Z" fill="#BC822A"/>
|
||||
<mask id="mask1_125_2968" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="1" y="3" width="22" height="18">
|
||||
<path d="M1.5 4.5C1.5 3.67157 2.17157 3 3 3H21C21.8284 3 22.5 3.67157 22.5 4.5V19.5C22.5 20.3284 21.8284 21 21 21H3C2.17157 21 1.5 20.3284 1.5 19.5V4.5Z" fill="#BC822A"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_125_2968)">
|
||||
<g filter="url(#filter1_dd_125_2968)">
|
||||
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="#D9D9D9"/>
|
||||
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="url(#paint0_linear_125_2968)"/>
|
||||
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="url(#paint1_linear_125_2968)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="#D9D9D9"/>
|
||||
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="url(#paint2_linear_125_2968)"/>
|
||||
<path d="M0 7.5C0 6.67157 0.671573 6 1.5 6H22.5C23.3284 6 24 6.67157 24 7.5V22.5C24 23.3284 23.3284 24 22.5 24H1.5C0.671573 24 0 23.3284 0 22.5V7.5Z" fill="url(#paint3_linear_125_2968)"/>
|
||||
<g filter="url(#filter2_dd_125_2968)">
|
||||
<path d="M12 9C12.8284 9 13.5 9.67157 13.5 10.5V15.8789L14.6895 14.6895C15.2752 14.1037 16.2248 14.1037 16.8105 14.6895C17.3963 15.2752 17.3963 16.2248 16.8105 16.8105L13.0605 20.5605C12.4748 21.1463 11.5252 21.1463 10.9395 20.5605L7.18945 16.8105C6.60367 16.2248 6.60367 15.2752 7.18945 14.6895C7.77524 14.1037 8.72476 14.1037 9.31055 14.6895L10.5 15.8789V10.5C10.5 9.67157 11.1716 9 12 9Z" fill="url(#paint4_linear_125_2968)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_125_2968" x="0.5" y="1.505" width="23" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.495"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2968"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.105"/>
|
||||
<feGaussianBlur stdDeviation="0.05"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_125_2968" result="effect2_dropShadow_125_2968"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2968" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_dd_125_2968" x="-1" y="4.505" width="26" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.495"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2968"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.105"/>
|
||||
<feGaussianBlur stdDeviation="0.05"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_125_2968" result="effect2_dropShadow_125_2968"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2968" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_dd_125_2968" x="5.75" y="8.5" width="12.5" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_125_2968"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.1"/>
|
||||
<feGaussianBlur stdDeviation="0.05"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_125_2968" result="effect2_dropShadow_125_2968"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_125_2968" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_125_2968" x1="0" y1="6" x2="17.28" y2="29.04" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DEB678"/>
|
||||
<stop offset="1" stop-color="#C59141"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_125_2968" x1="6.75" y1="3.75" x2="14.25" y2="26.25" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DEB678"/>
|
||||
<stop offset="1" stop-color="#B57F2D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_125_2968" x1="0" y1="6" x2="17.28" y2="29.04" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DEB678"/>
|
||||
<stop offset="1" stop-color="#C59141"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_125_2968" x1="6.75" y1="3.75" x2="14.25" y2="26.25" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DEB678"/>
|
||||
<stop offset="1" stop-color="#B57F2D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_125_2968" x1="9.13631" y1="7.22729" x2="12.8104" y2="20.0865" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.270833" stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F0F0F0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_125_2968">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,76 @@
|
||||
// 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.ComponentModel;
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public partial class CheckBoxWithDescriptionControl : CheckBox
|
||||
{
|
||||
private CheckBoxWithDescriptionControl _checkBoxSubTextControl;
|
||||
|
||||
public CheckBoxWithDescriptionControl()
|
||||
{
|
||||
_checkBoxSubTextControl = (CheckBoxWithDescriptionControl)this;
|
||||
this.Loaded += CheckBoxSubTextControl_Loaded;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
Update();
|
||||
base.OnApplyTemplate();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Header))
|
||||
{
|
||||
AutomationProperties.SetName(this, Header);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckBoxSubTextControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
StackPanel panel = new StackPanel() { Orientation = Orientation.Vertical };
|
||||
panel.Children.Add(new TextBlock() { Text = Header, TextWrapping = TextWrapping.WrapWholeWords });
|
||||
|
||||
// Add text box only if the description is not empty. Required for additional plugin options.
|
||||
if (!string.IsNullOrWhiteSpace(Description))
|
||||
{
|
||||
panel.Children.Add(new IsEnabledTextBlock() { Style = (Style)App.Current.Resources["SecondaryIsEnabledTextBlockStyle"], Text = Description });
|
||||
}
|
||||
|
||||
_checkBoxSubTextControl.Content = panel;
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
|
||||
"Header",
|
||||
typeof(string),
|
||||
typeof(CheckBoxWithDescriptionControl),
|
||||
new PropertyMetadata(default(string)));
|
||||
|
||||
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
|
||||
"Description",
|
||||
typeof(string),
|
||||
typeof(CheckBoxWithDescriptionControl),
|
||||
new PropertyMetadata(default(string)));
|
||||
|
||||
[Localizable(true)]
|
||||
public string Header
|
||||
{
|
||||
get => (string)GetValue(HeaderProperty);
|
||||
set => SetValue(HeaderProperty, value);
|
||||
}
|
||||
|
||||
[Localizable(true)]
|
||||
public string Description
|
||||
{
|
||||
get => (string)GetValue(DescriptionProperty);
|
||||
set => SetValue(DescriptionProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@
|
||||
SelectedValue="{x:Bind ViewModel.CurrentFilter, Mode=OneWay}"
|
||||
SelectionChanged="FiltersComboBox_SelectionChanged"
|
||||
Style="{StaticResource ComboBoxStyle}"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}, FallbackValue=Collapsed}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultComboBoxItemStyle}" TargetType="ComboBoxItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls">
|
||||
|
||||
<Style x:Key="DefaultIsEnabledTextBlockStyle" TargetType="controls:IsEnabledTextBlock">
|
||||
<Setter Property="Foreground" Value="{ThemeResource DefaultTextForegroundThemeBrush}" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:IsEnabledTextBlock">
|
||||
<Grid>
|
||||
<TextBlock
|
||||
x:Name="Label"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
Text="{TemplateBinding Text}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="Label.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SecondaryIsEnabledTextBlockStyle"
|
||||
BasedOn="{StaticResource DefaultIsEnabledTextBlockStyle}"
|
||||
TargetType="controls:IsEnabledTextBlock">
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.ComponentModel;
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
|
||||
public partial class IsEnabledTextBlock : Control
|
||||
{
|
||||
public IsEnabledTextBlock()
|
||||
{
|
||||
this.Style = (Style)App.Current.Resources["DefaultIsEnabledTextBlockStyle"];
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
IsEnabledChanged -= IsEnabledTextBlock_IsEnabledChanged;
|
||||
SetEnabledState();
|
||||
IsEnabledChanged += IsEnabledTextBlock_IsEnabledChanged;
|
||||
base.OnApplyTemplate();
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
|
||||
"Text",
|
||||
typeof(string),
|
||||
typeof(IsEnabledTextBlock),
|
||||
null);
|
||||
|
||||
[Localizable(true)]
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
private void IsEnabledTextBlock_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
SetEnabledState();
|
||||
}
|
||||
|
||||
private void SetEnabledState()
|
||||
{
|
||||
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,16 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.PageDown)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigatePageDownCommand>();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.PageUp)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigatePageUpCommand>();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
if (InSuggestion)
|
||||
{
|
||||
|
||||
@@ -78,6 +78,12 @@ public sealed partial class ContentPage : Page,
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
// Clean-up event listeners
|
||||
if (e.NavigationMode != NavigationMode.New)
|
||||
{
|
||||
ViewModel?.SafeCleanup();
|
||||
CleanupHelper.Cleanup(this);
|
||||
}
|
||||
|
||||
ViewModel = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
@@ -25,6 +26,8 @@ namespace Microsoft.CmdPal.UI;
|
||||
public sealed partial class ListPage : Page,
|
||||
IRecipient<NavigateNextCommand>,
|
||||
IRecipient<NavigatePreviousCommand>,
|
||||
IRecipient<NavigatePageDownCommand>,
|
||||
IRecipient<NavigatePageUpCommand>,
|
||||
IRecipient<ActivateSelectedListItemMessage>,
|
||||
IRecipient<ActivateSecondaryCommandMessage>
|
||||
{
|
||||
@@ -82,6 +85,8 @@ public sealed partial class ListPage : Page,
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePageDownCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigatePageUpCommand>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
@@ -94,6 +99,8 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePageDownCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<NavigatePageUpCommand>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSelectedListItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ActivateSecondaryCommandMessage>(this);
|
||||
|
||||
@@ -181,9 +188,9 @@ public sealed partial class ListPage : Page,
|
||||
var notificationText = li.Title;
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
ItemsList,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,6 +303,142 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigatePageDownCommand message)
|
||||
{
|
||||
var indexes = CalculateTargetIndexPageUpDownScrollTo(true);
|
||||
if (indexes is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(NavigatePageUpCommand message)
|
||||
{
|
||||
var indexes = CalculateTargetIndexPageUpDownScrollTo(false);
|
||||
if (indexes is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the item index to target when performing a page up or page down
|
||||
/// navigation. The calculation attempts to estimate how many items fit into
|
||||
/// the visible viewport by measuring actual container heights currently visible
|
||||
/// within the internal ScrollViewer. If measurements are not available a
|
||||
/// fallback estimate is used.
|
||||
/// </summary>
|
||||
/// <param name="isPageDown">True to calculate a page-down target, false for page-up.</param>
|
||||
/// <returns>
|
||||
/// A tuple containing the current index and the calculated target index, or null
|
||||
/// if a valid calculation could not be performed (for example, missing ScrollViewer).
|
||||
/// </returns>
|
||||
private (int CurrentIndex, int TargetIndex)? CalculateTargetIndexPageUpDownScrollTo(bool isPageDown)
|
||||
{
|
||||
var scroll = FindScrollViewer(ItemView);
|
||||
if (scroll is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var viewportHeight = scroll.ViewportHeight;
|
||||
if (viewportHeight <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var currentIndex = ItemView.SelectedIndex < 0 ? 0 : ItemView.SelectedIndex;
|
||||
var itemCount = ItemView.Items.Count;
|
||||
|
||||
// Compute visible item heights within the ScrollViewer viewport
|
||||
const int firstVisibleIndexNotFound = -1;
|
||||
var firstVisibleIndex = firstVisibleIndexNotFound;
|
||||
var visibleHeights = new List<double>(itemCount);
|
||||
|
||||
for (var i = 0; i < itemCount; i++)
|
||||
{
|
||||
if (ItemView.ContainerFromIndex(i) is FrameworkElement container)
|
||||
{
|
||||
try
|
||||
{
|
||||
var transform = container.TransformToVisual(scroll);
|
||||
var topLeft = transform.TransformPoint(new Point(0, 0));
|
||||
var bottom = topLeft.Y + container.ActualHeight;
|
||||
|
||||
// If any part of the container is inside the viewport, consider it visible
|
||||
if (topLeft.Y >= 0 && bottom <= viewportHeight)
|
||||
{
|
||||
if (firstVisibleIndex == firstVisibleIndexNotFound)
|
||||
{
|
||||
firstVisibleIndex = i;
|
||||
}
|
||||
|
||||
visibleHeights.Add(container.ActualHeight > 0 ? container.ActualHeight : 0);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore transform errors and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var itemsPerPage = 0;
|
||||
|
||||
// Calculate how many items fit in the viewport based on their actual heights
|
||||
if (visibleHeights.Count > 0)
|
||||
{
|
||||
double accumulated = 0;
|
||||
for (var i = 0; i < visibleHeights.Count; i++)
|
||||
{
|
||||
accumulated += visibleHeights[i] <= 0 ? 1 : visibleHeights[i];
|
||||
itemsPerPage++;
|
||||
if (accumulated >= viewportHeight)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback: estimate using first measured container height
|
||||
double itemHeight = 0;
|
||||
for (var i = currentIndex; i < itemCount; i++)
|
||||
{
|
||||
if (ItemView.ContainerFromIndex(i) is FrameworkElement { ActualHeight: > 0 } c)
|
||||
{
|
||||
itemHeight = c.ActualHeight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (itemHeight <= 0)
|
||||
{
|
||||
itemHeight = 1;
|
||||
}
|
||||
|
||||
itemsPerPage = Math.Max(1, (int)Math.Floor(viewportHeight / itemHeight));
|
||||
}
|
||||
|
||||
var targetIndex = isPageDown
|
||||
? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage))
|
||||
: Math.Max(0, currentIndex - Math.Max(1, itemsPerPage));
|
||||
|
||||
return (currentIndex, targetIndex);
|
||||
}
|
||||
|
||||
private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ListPage @this)
|
||||
@@ -351,11 +494,11 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollViewer? FindScrollViewer(DependencyObject parent)
|
||||
private static ScrollViewer? FindScrollViewer(DependencyObject parent)
|
||||
{
|
||||
if (parent is ScrollViewer)
|
||||
if (parent is ScrollViewer viewer)
|
||||
{
|
||||
return (ScrollViewer)parent;
|
||||
return viewer;
|
||||
}
|
||||
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
||||
/// <summary>
|
||||
/// A class that listens for local keyboard events using a Windows hook.
|
||||
/// </summary>
|
||||
internal sealed class LocalKeyboardListener : IDisposable
|
||||
internal sealed partial class LocalKeyboardListener : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Event that is raised when a key is pressed down.
|
||||
|
||||
@@ -22,7 +22,7 @@ internal static class WindowExtensions
|
||||
appWindow.SetIcon(@"Assets\icon.ico");
|
||||
}
|
||||
|
||||
private static HWND GetWindowHwnd(this Window window)
|
||||
public static HWND GetWindowHwnd(this Window window)
|
||||
{
|
||||
return window is null
|
||||
? throw new ArgumentNullException(nameof(window))
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides behavior to control taskbar and Alt+Tab presence by assigning a hidden owner
|
||||
/// and toggling extended window styles for a target window.
|
||||
/// </summary>
|
||||
internal sealed class HiddenOwnerWindowBehavior
|
||||
{
|
||||
private HWND _hiddenOwnerHwnd;
|
||||
private Window? _hiddenWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Shows or hides a window in the taskbar (and Alt+Tab) by updating ownership and extended window styles.
|
||||
/// </summary>
|
||||
/// <param name="target">The <see cref="Microsoft.UI.Xaml.Window"/> to update.</param>
|
||||
/// <param name="isVisibleInTaskbar"> True to show the window in the taskbar (and Alt+Tab); false to hide it from both. </param>
|
||||
/// <remarks>
|
||||
/// When hiding the window, a hidden owner is assigned and <see cref="WINDOW_EX_STYLE.WS_EX_TOOLWINDOW"/>
|
||||
/// is enabled to keep it out of the taskbar and Alt+Tab. When showing, the owner is cleared and
|
||||
/// <see cref="WINDOW_EX_STYLE.WS_EX_APPWINDOW"/> is enabled to ensure taskbar presence. Since tool
|
||||
/// windows use smaller corner radii, the normal rounded corners are enforced via
|
||||
/// <see cref="DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND"/>.
|
||||
/// </remarks>
|
||||
/// <seealso href="https://learn.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttons" />
|
||||
public void ShowInTaskbar(Window target, bool isVisibleInTaskbar)
|
||||
{
|
||||
/*
|
||||
* There are the three main ways to control whether a window appears on the taskbar:
|
||||
* https://learn.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttons
|
||||
*
|
||||
* 1. Set the window's owner. Owned windows do not appear on the taskbar:
|
||||
* Turns out this is the most reliable way to hide a window from the taskbar and ALT+TAB. WinForms and WPF uses this method
|
||||
* to back their ShowInTaskbar property as well.
|
||||
*
|
||||
* 2. Use the WS_EX_TOOLWINDOW extended window style:
|
||||
* This mostly works, with some reports that it silently fails in some cases. The biggest issue
|
||||
* is that for certain Windows settings (like Multitasking -> Show taskbar buttons on all displays = On all desktops),
|
||||
* the taskbar button is always shown even for tool windows.
|
||||
*
|
||||
* 3. Using ITaskbarList:
|
||||
* This is what AppWindow.IsShownInSwitchers uses, but it's COM-based and more complex, and can
|
||||
* fail if Explorer isn't running or responding. It could be a good backup, if needed.
|
||||
*/
|
||||
|
||||
var visibleHwnd = target.GetWindowHwnd();
|
||||
|
||||
if (isVisibleInTaskbar)
|
||||
{
|
||||
// remove any owner window
|
||||
PInvoke.SetWindowLongPtr(visibleHwnd, WINDOW_LONG_PTR_INDEX.GWLP_HWNDPARENT, HWND.Null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the hidden window as the owner of the target window
|
||||
var hiddenHwnd = EnsureHiddenOwner();
|
||||
PInvoke.SetWindowLongPtr(visibleHwnd, WINDOW_LONG_PTR_INDEX.GWLP_HWNDPARENT, hiddenHwnd);
|
||||
}
|
||||
|
||||
// Tool windows don't show up in ALT+TAB, and don't show up in the taskbar
|
||||
// Tool window and app window styles are mutually exclusive, change both just to be safe
|
||||
target.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, !isVisibleInTaskbar);
|
||||
target.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_APPWINDOW, isVisibleInTaskbar);
|
||||
|
||||
// Since tool windows have smaller corner radii, we need to force the normal ones
|
||||
target.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND);
|
||||
}
|
||||
|
||||
private HWND EnsureHiddenOwner()
|
||||
{
|
||||
if (_hiddenOwnerHwnd.IsNull)
|
||||
{
|
||||
_hiddenWindow = new Window();
|
||||
_hiddenOwnerHwnd = _hiddenWindow.GetWindowHwnd();
|
||||
}
|
||||
|
||||
return _hiddenOwnerHwnd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// 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.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.DestroyIconSafeHandle")]
|
||||
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.DestroyMenuSafeHandle")]
|
||||
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.FreeLibrarySafeHandle")]
|
||||
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.UnhookWindowsHookExSafeHandle")]
|
||||
@@ -57,14 +57,18 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private readonly List<TopLevelHotkey> _hotkeys = [];
|
||||
private readonly KeyboardListener _keyboardListener;
|
||||
private readonly LocalKeyboardListener _localKeyboardListener;
|
||||
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
|
||||
private bool _ignoreHotKeyWhenFullScreen = true;
|
||||
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
|
||||
private WindowPosition _currentWindowPosition = new();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
HideWindow();
|
||||
|
||||
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
|
||||
|
||||
@@ -73,6 +77,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
|
||||
}
|
||||
|
||||
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
|
||||
|
||||
_keyboardListener = new KeyboardListener();
|
||||
_keyboardListener.Start();
|
||||
|
||||
@@ -80,7 +86,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
this.SetIcon();
|
||||
AppWindow.Title = RS_.GetString("AppName");
|
||||
PositionCentered();
|
||||
RestoreWindowPosition();
|
||||
UpdateWindowPositionInMemory();
|
||||
|
||||
SetAcrylic();
|
||||
|
||||
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
|
||||
@@ -126,16 +134,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
|
||||
HideWindow();
|
||||
|
||||
ApplyWindowStyle();
|
||||
}
|
||||
|
||||
private void ApplyWindowStyle()
|
||||
{
|
||||
// Tool windows don't show up in ALT+TAB, and don't show up in the taskbar
|
||||
// Since tool windows have smaller corner radii, we need to force the normal ones
|
||||
this.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, !Debugger.IsAttached);
|
||||
this.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND);
|
||||
}
|
||||
|
||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||
@@ -161,6 +159,39 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PositionCentered(displayArea);
|
||||
}
|
||||
|
||||
private void RestoreWindowPosition()
|
||||
{
|
||||
var settings = App.Current.Services.GetService<SettingsModel>();
|
||||
if (settings?.LastWindowPosition is not WindowPosition savedPosition)
|
||||
{
|
||||
PositionCentered();
|
||||
return;
|
||||
}
|
||||
|
||||
if (savedPosition.Width <= 0 || savedPosition.Height <= 0)
|
||||
{
|
||||
PositionCentered();
|
||||
return;
|
||||
}
|
||||
|
||||
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
|
||||
|
||||
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
|
||||
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
|
||||
var workArea = displayArea.WorkArea;
|
||||
|
||||
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
|
||||
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
|
||||
|
||||
var targetPoint = new PointInt32
|
||||
{
|
||||
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
|
||||
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
|
||||
};
|
||||
|
||||
AppWindow.Move(targetPoint);
|
||||
}
|
||||
|
||||
private void PositionCentered(DisplayArea displayArea)
|
||||
{
|
||||
if (displayArea is not null)
|
||||
@@ -175,6 +206,17 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateWindowPositionInMemory()
|
||||
{
|
||||
_currentWindowPosition = new WindowPosition
|
||||
{
|
||||
X = AppWindow.Position.X,
|
||||
Y = AppWindow.Position.Y,
|
||||
Width = AppWindow.Size.Width,
|
||||
Height = AppWindow.Size.Height,
|
||||
};
|
||||
}
|
||||
|
||||
private void HotReloadSettings()
|
||||
{
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
@@ -257,14 +299,22 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
|
||||
}
|
||||
|
||||
var display = GetScreen(hwnd, target);
|
||||
PositionCentered(display);
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
{
|
||||
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
|
||||
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = GetScreen(hwnd, target);
|
||||
PositionCentered(display);
|
||||
}
|
||||
|
||||
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
|
||||
// because that would make it hard to debug the app
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
ApplyWindowStyle();
|
||||
_hiddenOwnerBehavior.ShowInTaskbar(this, true);
|
||||
}
|
||||
|
||||
// Just to be sure, SHOW our hwnd.
|
||||
@@ -419,6 +469,22 @@ public sealed partial class MainWindow : WindowEx,
|
||||
internal void MainWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
UpdateWindowPositionInMemory();
|
||||
|
||||
var settings = serviceProvider.GetService<SettingsModel>();
|
||||
if (settings is not null)
|
||||
{
|
||||
settings.LastWindowPosition = new WindowPosition
|
||||
{
|
||||
X = _currentWindowPosition.X,
|
||||
Y = _currentWindowPosition.Y,
|
||||
Width = _currentWindowPosition.Width,
|
||||
Height = _currentWindowPosition.Height,
|
||||
};
|
||||
|
||||
SettingsModel.SaveSettings(settings);
|
||||
}
|
||||
|
||||
var extensionService = serviceProvider.GetService<IExtensionService>()!;
|
||||
extensionService.SignalStopExtensionsAsync();
|
||||
|
||||
@@ -490,6 +556,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
// Save the current window position before hiding the window
|
||||
UpdateWindowPositionInMemory();
|
||||
|
||||
// If there's a debugger attached...
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
{
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Controls\ActionBar.xaml" />
|
||||
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="IsEnabledTextBlock.xaml" />
|
||||
<None Remove="ListDetailPage.xaml" />
|
||||
<None Remove="LoadingPage.xaml" />
|
||||
<None Remove="MainPage.xaml" />
|
||||
@@ -179,6 +181,27 @@
|
||||
<Content Update="..\Microsoft.CmdPal.UI.ViewModels\Assets\template.zip">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\StoreLogo.dark.svg">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\StoreLogo.light.svg">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\WinGetLogo.svg">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="IsEnabledTextBlock.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\KeyVisual\KeyCharPresenter.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Colors.xaml">
|
||||
|
||||
@@ -58,4 +58,9 @@ GetModuleHandle
|
||||
|
||||
GetWindowLong
|
||||
SetWindowLong
|
||||
WINDOW_EX_STYLE
|
||||
WINDOW_EX_STYLE
|
||||
CreateWindowEx
|
||||
WNDCLASSEXW
|
||||
RegisterClassEx
|
||||
GetStockObject
|
||||
GetModuleHandle
|
||||
@@ -108,6 +108,7 @@
|
||||
Visibility="{x:Bind IsText, Mode=OneWay}" />
|
||||
<HyperlinkButton
|
||||
Padding="0"
|
||||
Command="{x:Bind NavigateCommand, Mode=OneWay}"
|
||||
NavigateUri="{x:Bind Link, Mode=OneWay}"
|
||||
Visibility="{x:Bind IsLink, Mode=OneWay}">
|
||||
<TextBlock Text="{x:Bind Text, Mode=OneWay}" TextWrapping="Wrap" />
|
||||
|
||||
@@ -48,7 +48,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IRecipient<ShowConfirmationMessage>,
|
||||
IRecipient<ShowToastMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
INotifyPropertyChanged
|
||||
INotifyPropertyChanged,
|
||||
IDisposable
|
||||
{
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
@@ -65,6 +66,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
private SettingsWindow? _settingsWindow;
|
||||
|
||||
private CancellationTokenSource? _focusAfterLoadedCts;
|
||||
private WeakReference<Page>? _lastNavigatedPageRef;
|
||||
|
||||
public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService<ShellViewModel>()!;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
@@ -447,7 +451,15 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
{
|
||||
while (RootFrame.CanGoBack)
|
||||
{
|
||||
GoBack(withAnimation, focusSearch);
|
||||
// don't focus on each step, just at the end
|
||||
GoBack(withAnimation, focusSearch: false);
|
||||
}
|
||||
|
||||
// focus search box, even if we were already home
|
||||
if (focusSearch)
|
||||
{
|
||||
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
SearchBox.SelectSearch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,6 +500,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
if (e.Content is Page element)
|
||||
{
|
||||
_lastNavigatedPageRef = new WeakReference<Page>(element);
|
||||
element.Loaded += FocusAfterLoaded;
|
||||
}
|
||||
}
|
||||
@@ -497,6 +510,18 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
var page = (Page)sender;
|
||||
page.Loaded -= FocusAfterLoaded;
|
||||
|
||||
// Only handle focus for the latest navigated page
|
||||
if (_lastNavigatedPageRef is null || !_lastNavigatedPageRef.TryGetTarget(out var last) || !ReferenceEquals(page, last))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any previous pending focus work
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
_focusAfterLoadedCts = new CancellationTokenSource();
|
||||
var token = _focusAfterLoadedCts.Token;
|
||||
|
||||
AnnounceNavigationToPage(page);
|
||||
|
||||
var shouldSearchBoxBeVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
|
||||
@@ -509,34 +534,57 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await page.DispatcherQueue.EnqueueAsync(async () =>
|
||||
_ = Task.Run(
|
||||
async () =>
|
||||
{
|
||||
// I hate this so much, but it can take a while for the page to be ready to accept focus;
|
||||
// focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts)
|
||||
for (var i = 0; i < 10; i++)
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
|
||||
{
|
||||
var set = frameworkElement.Focus(FocusState.Programmatic);
|
||||
if (set)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the search box visibility based on the current page:
|
||||
// - We do this here after navigation so the focus is not jumping around too much,
|
||||
// it messes with screen readers if we do it too early
|
||||
// - Since this should hide the search box on content pages, it's not a problem if we
|
||||
// wait for the code above to finish trying to focus the content
|
||||
ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
|
||||
});
|
||||
});
|
||||
try
|
||||
{
|
||||
await page.DispatcherQueue.EnqueueAsync(
|
||||
async () =>
|
||||
{
|
||||
// I hate this so much, but it can take a while for the page to be ready to accept focus;
|
||||
// focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts)
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
|
||||
{
|
||||
var set = frameworkElement.Focus(FocusState.Programmatic);
|
||||
if (set)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(100, token);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Update the search box visibility based on the current page:
|
||||
// - We do this here after navigation so the focus is not jumping around too much,
|
||||
// it messes with screen readers if we do it too early
|
||||
// - Since this should hide the search box on content pages, it's not a problem if we
|
||||
// wait for the code above to finish trying to focus the content
|
||||
ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Swallow cancellation - another FocusAfterLoaded invocation superseded this one
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error during FocusAfterLoaded async focus work", ex);
|
||||
}
|
||||
},
|
||||
token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,24 +639,31 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
|
||||
|
||||
var onlyAlt = altPressed && !ctrlPressed && !shiftPressed && !winPressed;
|
||||
if (e.Key == VirtualKey.Left && onlyAlt)
|
||||
var onlyCtrl = !altPressed && ctrlPressed && !shiftPressed && !winPressed;
|
||||
switch (e.Key)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Home && onlyAlt)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(WithAnimation: false));
|
||||
e.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
case VirtualKey.Left when onlyAlt: // Alt+Left arrow
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
e.Handled = true;
|
||||
break;
|
||||
case VirtualKey.Home when onlyAlt: // Alt+Home
|
||||
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(WithAnimation: false));
|
||||
e.Handled = true;
|
||||
break;
|
||||
case (VirtualKey)188 when onlyCtrl: // Ctrl+,
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
e.Handled = true;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,4 +713,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
Logger.LogError("Error handling mouse button press event", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
_focusAfterLoadedCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
x:Key="StringEmptyToBoolConverter"
|
||||
EmptyValue="False"
|
||||
NotEmptyValue="True" />
|
||||
<converters:BoolToObjectConverter
|
||||
x:Key="BoolToOptionConverter"
|
||||
FalseValue="1"
|
||||
TrueValue="0" />
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
|
||||
@@ -47,22 +51,34 @@
|
||||
MaxWidth="1000"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<controls:SettingsCard x:Uid="ExtensionEnableCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:SettingsCard x:Uid="ExtensionEnableCard">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<cpcontrols:ContentIcon>
|
||||
<cpcontrols:ContentIcon.Content>
|
||||
<cpcontrols:IconBox
|
||||
Width="20"
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind ViewModel.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<controls:SettingsCard.Header>
|
||||
<TextBlock>
|
||||
<Run x:Uid="ExtensionEnable" Text="Enable" />
|
||||
<Run Text="{x:Bind ViewModel.DisplayName}" />
|
||||
</TextBlock>
|
||||
</controls:SettingsCard.Header>
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
TargetType="x:Boolean"
|
||||
Value="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
|
||||
<controls:Case Value="True">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
|
||||
<TextBlock x:Uid="ExtensionCommandsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<ItemsRepeater ItemsSource="{x:Bind ViewModel.TopLevelCommands, Mode=OneWay}" Layout="{StaticResource VerticalStackLayout}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewModels:TopLevelViewModel">
|
||||
@@ -84,22 +100,22 @@
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
|
||||
<!-- Content goes here -->
|
||||
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard x:Uid="Settings_ExtensionPage_GlobalHotkey_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:SettingsCard x:Uid="Settings_ExtensionPage_GlobalHotkey_SettingsCard">
|
||||
<cpcontrols:ShortcutControl HotkeySettings="{x:Bind Hotkey, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_ExtensionPage_Alias_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBox Text="{x:Bind AliasText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<ToggleSwitch
|
||||
x:Uid="Settings_ExtensionPage_Alias_ToggleSwitch"
|
||||
IsEnabled="{x:Bind AliasText, Converter={StaticResource StringEmptyToBoolConverter}, Mode=OneWay}"
|
||||
IsOn="{x:Bind IsDirectAlias, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
<controls:SettingsCard x:Uid="Settings_ExtensionPage_Alias_SettingsCard">
|
||||
<TextBox
|
||||
x:Uid="Settings_ExtensionPage_Alias_PlaceholderText"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Text="{x:Bind AliasText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_ExtensionPage_AliasActivation_SettingsCard" IsEnabled="{x:Bind AliasText, Converter={StaticResource StringEmptyToBoolConverter}, Mode=OneWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind IsDirectAlias, Converter={StaticResource BoolToOptionConverter}, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_ExtensionPage_Alias_DirectComboBox" />
|
||||
<ComboBoxItem x:Uid="Settings_ExtensionPage_Alias_IndirectComboBox" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
</DataTemplate>
|
||||
@@ -130,11 +146,8 @@
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
|
||||
<!-- Content goes here -->
|
||||
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
|
||||
|
||||
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
@@ -145,8 +158,13 @@
|
||||
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
|
||||
Visibility="{x:Bind ViewModel.HasSettings}" />
|
||||
|
||||
<Frame x:Name="SettingsFrame" Visibility="{x:Bind ViewModel.HasSettings}">
|
||||
|
||||
<Frame
|
||||
x:Name="SettingsFrame"
|
||||
Background="{ThemeResource SettingsCardBackground}"
|
||||
BorderBrush="{ThemeResource SettingsCardBorderBrush}"
|
||||
BorderThickness="{ThemeResource SettingsCardBorderThickness}"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind ViewModel.HasSettings}">
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
TargetType="x:Boolean"
|
||||
@@ -163,7 +181,6 @@
|
||||
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
|
||||
</Frame>
|
||||
|
||||
<TextBlock
|
||||
@@ -197,8 +214,6 @@
|
||||
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
|
||||
Visibility="{x:Bind ViewModel.IsFromExtension, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
<controls:SettingsCard x:Uid="Settings_ExtensionPage_Builtin_SettingsCard" Visibility="{x:Bind ViewModel.IsFromExtension, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -14,19 +14,204 @@
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<!-- ControlFillColorQuarternaryBrush does not exist (yet) in WinUI, only in Windows Visual Library (Figma) -->
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<LinearGradientBrush x:Key="CardGradient2Brush" StartPoint="0,0" EndPoint="0.5, 1">
|
||||
<GradientStop Offset="0" Color="#38C8AEC4" />
|
||||
<GradientStop Offset="1" Color="#383286EE" />
|
||||
</LinearGradientBrush>
|
||||
<ImageSource x:Key="StoreLogo">ms-appx:///Assets/StoreLogo.dark.svg</ImageSource>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<LinearGradientBrush x:Key="CardGradient2Brush" StartPoint="0,0" EndPoint="1, 1">
|
||||
<!--<GradientStop Offset="0" Color="#E6F0FC" />
|
||||
<GradientStop Offset="0.4" Color="#E6F0FC" />
|
||||
<GradientStop Offset="1" Color="#F0F0F7" />-->
|
||||
<GradientStop Offset="0.0" Color="#FFF6F9FF" />
|
||||
<!-- Light cool white -->
|
||||
<GradientStop Offset="0.4" Color="#FFEFF5FF" />
|
||||
<!-- Hinted lavender/blue -->
|
||||
<GradientStop Offset="0.7" Color="#FFF7FAFD" />
|
||||
<!-- Very soft neutral white -->
|
||||
<GradientStop Offset="1.0" Color="#FFF5F8FA" />
|
||||
<!-- Slight bluish-gray tint -->
|
||||
<!-- Faint peach glow -->
|
||||
<!-- Soft peach -->
|
||||
</LinearGradientBrush>
|
||||
<ImageSource x:Key="StoreLogo">ms-appx:///Assets/StoreLogo.light.svg</ImageSource>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="CardGradient2Brush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="CardGradient1Brush" Color="Transparent" />
|
||||
<ImageSource x:Key="StoreLogo">ms-appx:///Assets/StoreLogo.dark.svg</ImageSource>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
<converters:BoolNegationConverter x:Key="InvertedBoolConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
|
||||
<ScrollViewer x:Name="RootScrollViewer" Grid.Row="1">
|
||||
<Grid Padding="16">
|
||||
<StackPanel
|
||||
MaxWidth="1000"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
<!-- Banner -->
|
||||
<Grid
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardGradient2Brush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel x:Name="BannerDescriptionPanel" Orientation="Vertical">
|
||||
<TextBlock x:Uid="Settings_ExtensionsPage_Banner_Header" FontWeight="SemiBold" />
|
||||
<TextBlock
|
||||
x:Uid="Settings_ExtensionsPage_Banner_Description"
|
||||
Grid.Row="1"
|
||||
MaxWidth="520"
|
||||
HorizontalAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
<HyperlinkButton
|
||||
x:Uid="Settings_ExtensionsPage_Banner_Hyperlink"
|
||||
Padding="0"
|
||||
Content="Learn how to create your own extensions"
|
||||
NavigateUri="https://learn.microsoft.com/windows/powertoys/command-palette/overview" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="ButtonPanel"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button x:Uid="Settings_ExtensionsPage_FindExtensions_MicrosoftStore" Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Viewbox Width="16">
|
||||
<Image AutomationProperties.AccessibilityView="Raw" Source="{ThemeResource StoreLogo}" />
|
||||
</Viewbox>
|
||||
<TextBlock Text="Microsoft Store" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<!--<Button x:Uid="Settings_ExtensionsPage_FindExtensions_WinGet" Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Viewbox Width="16">
|
||||
<Image AutomationProperties.AccessibilityView="Raw" Source="ms-appx:///Assets/WinGet.svg" />
|
||||
</Viewbox>
|
||||
<TextBlock Text="WinGet" />
|
||||
</StackPanel>
|
||||
</Button>-->
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ItemsRepeater ItemsSource="{x:Bind viewModel.CommandProviders, Mode=OneWay}" Layout="{StaticResource VerticalStackLayout}">
|
||||
<!-- Search -->
|
||||
<Grid Padding="0,24,0,8" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MaxWidth="320" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<AutoSuggestBox
|
||||
x:Name="SearchBox"
|
||||
x:Uid="Settings_ExtensionsPage_SearchBox_Placeholder"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{x:Bind viewModel.Extensions.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<AutoSuggestBox.QueryIcon>
|
||||
<SymbolIcon Symbol="Find" />
|
||||
</AutoSuggestBox.QueryIcon>
|
||||
<AutoSuggestBox.KeyboardAccelerators>
|
||||
<KeyboardAccelerator
|
||||
Key="F"
|
||||
Invoked="OnFindInvoked"
|
||||
Modifiers="Control" />
|
||||
</AutoSuggestBox.KeyboardAccelerators>
|
||||
</AutoSuggestBox>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<TextBlock
|
||||
x:Name="ItemCounterTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind viewModel.Extensions.ItemCounterText, Mode=OneWay}" />
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
Visibility="{x:Bind viewModel.Extensions.ShowManualReloadOverlay, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ProgressRing
|
||||
Width="16"
|
||||
Height="16"
|
||||
IsActive="True" />
|
||||
<TextBlock
|
||||
x:Uid="Settings_ExtensionsPage_Reloading_Text"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyTextBlockStyle}" />
|
||||
</StackPanel>
|
||||
<Button
|
||||
x:Name="MoreButton"
|
||||
x:Uid="Settings_ExtensionsPage_More_Button"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Button.Flyout>
|
||||
<MenuFlyout Placement="BottomEdgeAlignedRight">
|
||||
<MenuFlyoutItem x:Uid="Settings_ExtensionsPage_More_Reload_MenuFlyoutItem" Command="{x:Bind viewModel.Extensions.ReloadExtensionsCommand}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
<FontIcon
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="16"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Empty state when no results match the current search -->
|
||||
<Grid
|
||||
x:Name="NoResultsPanel"
|
||||
Padding="48"
|
||||
x:Load="{x:Bind viewModel.Extensions.ShowNoResultsPanel, Mode=OneWay}"
|
||||
CornerRadius="4">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<TextBlock
|
||||
x:Uid="Settings_ExtensionsPage_NoResults_Primary"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
x:Uid="Settings_ExtensionsPage_NoResults_Secondary"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ItemsRepeater
|
||||
x:Name="ProvidersRepeater"
|
||||
x:Load="{x:Bind viewModel.Extensions.HasResults, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind viewModel.Extensions.FilteredProviders, Mode=OneWay}"
|
||||
Layout="{StaticResource VerticalStackLayout}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewModels:ProviderSettingsViewModel">
|
||||
<controls:SettingsCard
|
||||
@@ -47,9 +232,7 @@
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
|
||||
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
|
||||
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
@@ -57,5 +240,25 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="LayoutVisualStates">
|
||||
<VisualState x:Name="WideLayout">
|
||||
<VisualState.StateTriggers>
|
||||
<AdaptiveTrigger MinWindowWidth="720" />
|
||||
</VisualState.StateTriggers>
|
||||
</VisualState>
|
||||
<VisualState x:Name="NarrowLayout">
|
||||
<VisualState.StateTriggers>
|
||||
<AdaptiveTrigger MinWindowWidth="0" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ButtonPanel.(Grid.Row)" Value="1" />
|
||||
<Setter Target="BannerDescriptionPanel.(Grid.ColumnSpan)" Value="2" />
|
||||
<Setter Target="ButtonPanel.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ButtonPanel.Margin" Value="0,12,0,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user