mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-18 10:07:56 +01:00
Compare commits
14 Commits
leilzh/spa
...
dev/crutka
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b34038f2b | ||
|
|
c6c7bfb861 | ||
|
|
d64f06906c | ||
|
|
c26dfef81b | ||
|
|
dd420509ab | ||
|
|
2c4aab9d87 | ||
|
|
52ce33d438 | ||
|
|
fc1307418e | ||
|
|
f45d54abdf | ||
|
|
f28d009131 | ||
|
|
bc8adb3189 | ||
|
|
b67d3b4418 | ||
|
|
ae6da3235b | ||
|
|
b1985bc8d1 |
11
.github/actions/spell-check/expect.txt
vendored
11
.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
|
||||
@@ -383,6 +387,7 @@ DISPLAYFREQUENCY
|
||||
displayname
|
||||
DISPLAYORIENTATION
|
||||
divyan
|
||||
djwsxzxb
|
||||
Dlg
|
||||
DLGFRAME
|
||||
DLGMODALFRAME
|
||||
@@ -443,6 +448,7 @@ EDITSHORTCUTS
|
||||
EDITTEXT
|
||||
EFile
|
||||
ekus
|
||||
eku
|
||||
emojis
|
||||
ENABLEDELAYEDEXPANSION
|
||||
ENABLEDPOPUP
|
||||
@@ -636,6 +642,7 @@ Hiber
|
||||
Hiberboot
|
||||
HIBYTE
|
||||
hicon
|
||||
HICONSM
|
||||
HIDEREADONLY
|
||||
HIDEWINDOW
|
||||
Hif
|
||||
@@ -816,6 +823,7 @@ keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
ksa
|
||||
kvp
|
||||
Kybd
|
||||
LARGEICON
|
||||
@@ -922,6 +930,7 @@ LWA
|
||||
lwin
|
||||
LZero
|
||||
MAGTRANSFORM
|
||||
makeappx
|
||||
MAKEINTRESOURCE
|
||||
MAKEINTRESOURCEA
|
||||
MAKEINTRESOURCEW
|
||||
@@ -1254,6 +1263,7 @@ pinvoke
|
||||
pipename
|
||||
PKBDLLHOOKSTRUCT
|
||||
pkgfamily
|
||||
PKI
|
||||
plib
|
||||
ploc
|
||||
ploca
|
||||
@@ -1695,6 +1705,7 @@ syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
SYSMENU
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
SYSTEMTIME
|
||||
|
||||
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": [
|
||||
|
||||
@@ -73,10 +73,11 @@ extends:
|
||||
parameters:
|
||||
pool:
|
||||
name: SHINE-INT-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
${{ else }}:
|
||||
image: SHINE-VS17-Latest
|
||||
demands:
|
||||
# Our INT agents have a large disk mounted at P:\
|
||||
- WorkFolder -equals P:\_work
|
||||
- ${{ if eq(parameters.useVSPreview, true) }}:
|
||||
- ImageOverride -equals SHINE-VS17-Preview
|
||||
os: windows
|
||||
variables:
|
||||
IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations
|
||||
|
||||
@@ -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}"
|
||||
@@ -867,6 +870,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
|
||||
|
||||
20
README.md
20
README.md
@@ -48,7 +48,7 @@ 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.
|
||||
@@ -56,17 +56,17 @@ Go to the [PowerToys GitHub releases][github-release-link], click Assets to reve
|
||||
<!-- 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.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.0/PowerToysUserSetup-0.95.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-arm64.exe
|
||||
[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.95.0-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.95.0-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.95.0-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.95.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>
|
||||
|
||||
@@ -281,4 +281,4 @@ The application logs basic diagnostic data (telemetry). For more privacy informa
|
||||
[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=
|
||||
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
|
||||
[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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
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" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
|
||||
<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="PowerToys.Settings.exe" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
|
||||
<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" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
|
||||
<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="powershell -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.
|
||||
@@ -3,6 +3,26 @@
|
||||
#include "ThemeHelper.h"
|
||||
|
||||
// Controls changing the themes.
|
||||
static void ResetColorPrevalence()
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD value = 0; // back to default value
|
||||
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void SetAppsTheme(bool mode)
|
||||
{
|
||||
@@ -36,6 +56,11 @@ void SetSystemTheme(bool mode)
|
||||
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
if (mode) // if are changing to light mode
|
||||
{
|
||||
ResetColorPrevalence();
|
||||
}
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
@@ -47,6 +47,7 @@ const static wchar_t* MODULE_DESC = L"This is a module that allows you to contro
|
||||
|
||||
enum class ScheduleMode
|
||||
{
|
||||
Off,
|
||||
FixedHours,
|
||||
SunsetToSunrise,
|
||||
// add more later
|
||||
@@ -59,8 +60,9 @@ inline std::wstring ToString(ScheduleMode mode)
|
||||
case ScheduleMode::SunsetToSunrise:
|
||||
return L"SunsetToSunrise";
|
||||
case ScheduleMode::FixedHours:
|
||||
default:
|
||||
return L"FixedHours";
|
||||
default:
|
||||
return L"Off";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +70,9 @@ inline ScheduleMode FromString(const std::wstring& str)
|
||||
{
|
||||
if (str == L"SunsetToSunrise")
|
||||
return ScheduleMode::SunsetToSunrise;
|
||||
return ScheduleMode::FixedHours;
|
||||
if (str == L"FixedHours")
|
||||
return ScheduleMode::FixedHours;
|
||||
return ScheduleMode::Off;
|
||||
}
|
||||
|
||||
// These are the properties shown in the Settings page.
|
||||
@@ -76,7 +80,7 @@ struct ModuleSettings
|
||||
{
|
||||
bool m_changeSystem = true;
|
||||
bool m_changeApps = true;
|
||||
ScheduleMode m_scheduleMode = ScheduleMode::FixedHours;
|
||||
ScheduleMode m_scheduleMode = ScheduleMode::Off;
|
||||
int m_lightTime = 480;
|
||||
int m_darkTime = 1200;
|
||||
int m_sunrise_offset = 0;
|
||||
@@ -161,7 +165,8 @@ public:
|
||||
L"scheduleMode",
|
||||
L"Theme schedule mode",
|
||||
ToString(g_settings.m_scheduleMode),
|
||||
{ { L"FixedHours", L"Set hours manually" },
|
||||
{ { L"Off", L"Disable the schedule" },
|
||||
{ L"FixedHours", L"Set hours manually" },
|
||||
{ L"SunsetToSunrise", L"Use sunrise/sunset times" } });
|
||||
|
||||
// Integer spinners
|
||||
@@ -284,9 +289,20 @@ public:
|
||||
g_settings.m_changeApps = *v;
|
||||
}
|
||||
|
||||
auto previousMode = g_settings.m_scheduleMode;
|
||||
|
||||
if (auto v = values.get_string_value(L"scheduleMode"))
|
||||
{
|
||||
g_settings.m_scheduleMode = FromString(*v);
|
||||
auto newMode = FromString(*v);
|
||||
if (newMode != g_settings.m_scheduleMode)
|
||||
{
|
||||
Logger::info(L"[LightSwitchInterface] Schedule mode changed from {} to {}",
|
||||
ToString(g_settings.m_scheduleMode),
|
||||
ToString(newMode));
|
||||
g_settings.m_scheduleMode = newMode;
|
||||
|
||||
start_service_if_needed();
|
||||
}
|
||||
}
|
||||
|
||||
if (auto v = values.get_int_value(L"lightTime"))
|
||||
@@ -304,7 +320,7 @@ public:
|
||||
g_settings.m_sunrise_offset = *v;
|
||||
}
|
||||
|
||||
if (auto v = values.get_int_value(L"m_sunset_offset"))
|
||||
if (auto v = values.get_int_value(L"sunset_offset"))
|
||||
{
|
||||
g_settings.m_sunset_offset = *v;
|
||||
}
|
||||
@@ -326,6 +342,47 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
virtual void start_service_if_needed()
|
||||
{
|
||||
if (!m_process || WaitForSingleObject(m_process, 0) != WAIT_TIMEOUT)
|
||||
{
|
||||
Logger::info(L"[LightSwitchInterface] Starting LightSwitchService due to active schedule mode.");
|
||||
enable();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"[LightSwitchInterface] Service already running, skipping start.");
|
||||
}
|
||||
}
|
||||
|
||||
/*virtual void stop_worker_only()
|
||||
{
|
||||
if (m_process)
|
||||
{
|
||||
Logger::info(L"[LightSwitchInterface] Stopping LightSwitchService (worker only).");
|
||||
constexpr DWORD timeout_ms = 1500;
|
||||
DWORD result = WaitForSingleObject(m_process, timeout_ms);
|
||||
|
||||
if (result == WAIT_TIMEOUT)
|
||||
{
|
||||
Logger::warn("Light Switch: Process didn't exit in time. Forcing termination.");
|
||||
TerminateProcess(m_process, 0);
|
||||
}
|
||||
|
||||
CloseHandle(m_process);
|
||||
m_process = nullptr;
|
||||
}
|
||||
}*/
|
||||
|
||||
/*virtual void stop_service_if_running()
|
||||
{
|
||||
if (m_process)
|
||||
{
|
||||
Logger::info(L"[LightSwitchInterface] Stopping LightSwitchService due to schedule OFF.");
|
||||
stop_worker_only();
|
||||
}
|
||||
}*/
|
||||
|
||||
virtual void enable()
|
||||
{
|
||||
m_enabled = true;
|
||||
@@ -413,6 +470,12 @@ public:
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
// Returns whether the PowerToys should be enabled by default
|
||||
virtual bool is_enabled_by_default() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
auto settingsObject = settings.get_raw_json();
|
||||
@@ -471,6 +534,15 @@ public:
|
||||
SetAppsTheme(!GetCurrentAppsTheme());
|
||||
}
|
||||
|
||||
if (!m_manual_override_event_handle)
|
||||
{
|
||||
m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
if (!m_manual_override_event_handle)
|
||||
{
|
||||
m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
}
|
||||
}
|
||||
|
||||
if (m_manual_override_event_handle)
|
||||
{
|
||||
SetEvent(m_manual_override_event_handle);
|
||||
|
||||
@@ -16,6 +16,8 @@ SERVICE_STATUS g_ServiceStatus = {};
|
||||
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
|
||||
HANDLE g_ServiceStopEvent = nullptr;
|
||||
static int g_lastUpdatedDay = -1;
|
||||
static ScheduleMode prevMode = ScheduleMode::Off;
|
||||
static std::wstring prevLat, prevLon;
|
||||
|
||||
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
|
||||
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
|
||||
@@ -185,62 +187,164 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
if (isLightActive)
|
||||
{
|
||||
if (settings.changeSystem && !isSystemCurrentlyLight)
|
||||
{
|
||||
SetSystemTheme(true);
|
||||
Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
|
||||
}
|
||||
if (settings.changeApps && !isAppsCurrentlyLight)
|
||||
{
|
||||
SetAppsTheme(true);
|
||||
Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (settings.changeSystem && isSystemCurrentlyLight)
|
||||
{
|
||||
SetSystemTheme(false);
|
||||
Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
|
||||
}
|
||||
if (settings.changeApps && isAppsCurrentlyLight)
|
||||
{
|
||||
SetAppsTheme(false);
|
||||
Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- At service start: immediately honor the schedule ---
|
||||
// --- Initial settings load ---
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
// --- Initial theme application (if schedule enabled) ---
|
||||
if (settings.scheduleMode != ScheduleMode::Off)
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change.");
|
||||
}
|
||||
|
||||
// --- Main loop: wakes once per minute or stop/parent death ---
|
||||
// --- Main loop ---
|
||||
for (;;)
|
||||
{
|
||||
HANDLE waits[2] = { g_ServiceStopEvent, hParent };
|
||||
DWORD count = hParent ? 2 : 1;
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
// Check for changes in schedule mode or coordinates
|
||||
bool modeChangedToSunset = (prevMode != settings.scheduleMode &&
|
||||
settings.scheduleMode == ScheduleMode::SunsetToSunrise);
|
||||
bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude);
|
||||
|
||||
if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times.");
|
||||
update_sun_times(settings);
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
g_lastUpdatedDay = st.wDay;
|
||||
prevMode = settings.scheduleMode;
|
||||
prevLat = settings.latitude;
|
||||
prevLon = settings.longitude;
|
||||
}
|
||||
|
||||
// If schedule is off, idle but keep watching settings and manual override
|
||||
if (settings.scheduleMode == ScheduleMode::Off)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive.");
|
||||
|
||||
if (!hManualOverride)
|
||||
{
|
||||
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
}
|
||||
|
||||
HANDLE waits[4];
|
||||
DWORD count = 0;
|
||||
waits[count++] = g_ServiceStopEvent;
|
||||
if (hParent)
|
||||
waits[count++] = hParent;
|
||||
if (hManualOverride)
|
||||
waits[count++] = hManualOverride;
|
||||
waits[count++] = LightSwitchSettings::instance().GetSettingsChangedEvent();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, INFINITE);
|
||||
|
||||
// --- Handle exit signals ---
|
||||
if (wait == WAIT_OBJECT_0) // stop event
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
|
||||
break;
|
||||
}
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent exited - stopping service.");
|
||||
break;
|
||||
}
|
||||
|
||||
// --- Manual override triggered ---
|
||||
if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Manual override received while schedule OFF.");
|
||||
ResetEvent(hManualOverride);
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Settings file changed ---
|
||||
if (wait == WAIT_OBJECT_0 + (hParent ? 3 : 2))
|
||||
{
|
||||
Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings...");
|
||||
|
||||
ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent());
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& newSettings = LightSwitchSettings::instance().settings();
|
||||
|
||||
if (newSettings.scheduleMode != ScheduleMode::Off)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule re-enabled, resuming normal loop.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- When schedule is active, run once per minute ---
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
// Refresh suntimes at day boundary
|
||||
if (g_lastUpdatedDay != st.wDay)
|
||||
if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise))
|
||||
{
|
||||
update_sun_times(settings);
|
||||
g_lastUpdatedDay = st.wDay;
|
||||
|
||||
Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
|
||||
}
|
||||
|
||||
// Have to do this again in case settings got updated in the refresh suntimes chunk
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& currentSettings = LightSwitchSettings::instance().settings();
|
||||
|
||||
wchar_t msg[160];
|
||||
swprintf_s(msg,
|
||||
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d",
|
||||
L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d",
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
settings.lightTime / 60,
|
||||
settings.lightTime % 60,
|
||||
settings.darkTime / 60,
|
||||
settings.darkTime % 60);
|
||||
currentSettings.lightTime / 60,
|
||||
currentSettings.lightTime % 60,
|
||||
currentSettings.darkTime / 60,
|
||||
currentSettings.darkTime % 60,
|
||||
static_cast<int>(currentSettings.scheduleMode));
|
||||
Logger::info(msg);
|
||||
|
||||
// --- Manual override check ---
|
||||
@@ -252,22 +356,20 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
|
||||
if (manualOverrideActive)
|
||||
{
|
||||
// Did we hit a scheduled boundary? (reset override at boundary)
|
||||
if (nowMinutes == (settings.lightTime + settings.sunrise_offset) % 1440 ||
|
||||
nowMinutes == (settings.darkTime + settings.sunset_offset) % 1440)
|
||||
if (nowMinutes == (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440 ||
|
||||
nowMinutes == (currentSettings.darkTime + currentSettings.sunset_offset) % 1440)
|
||||
{
|
||||
ResetEvent(hManualOverride);
|
||||
Logger::info(L"[LightSwitchService] Manual override cleared at boundary\n");
|
||||
Logger::info(L"[LightSwitchService] Manual override cleared at boundary");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override\n");
|
||||
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override");
|
||||
goto sleep_until_next_minute;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply theme logic (only runs if no manual override or override just cleared)
|
||||
applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
|
||||
applyTheme(nowMinutes, currentSettings.lightTime + currentSettings.sunrise_offset, currentSettings.darkTime + currentSettings.sunset_offset, currentSettings);
|
||||
|
||||
sleep_until_next_minute:
|
||||
GetLocalTime(&st);
|
||||
@@ -278,15 +380,14 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered <EFBFBD> exiting worker loop.");
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
|
||||
break;
|
||||
}
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent process exited
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent process exited <EFBFBD> stopping service.");
|
||||
Logger::info(L"[LightSwitchService] Parent process exited - stopping service.");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (hManualOverride)
|
||||
@@ -297,6 +398,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
||||
{
|
||||
if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <WinHookEventIDs.h>
|
||||
#include <logger.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -27,10 +28,20 @@ std::wstring LightSwitchSettings::GetSettingsFileName()
|
||||
|
||||
void LightSwitchSettings::InitFileWatcher()
|
||||
{
|
||||
const std::wstring& settingsFileName = GetSettingsFileName();
|
||||
m_settingsFileWatcher = std::make_unique<FileWatcher>(settingsFileName, [&]() {
|
||||
PostMessageW(HWND_BROADCAST, WM_PRIV_SETTINGS_CHANGED, NULL, NULL);
|
||||
});
|
||||
if (!m_settingsChangedEvent)
|
||||
{
|
||||
m_settingsChangedEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
}
|
||||
|
||||
if (!m_settingsFileWatcher)
|
||||
{
|
||||
m_settingsFileWatcher = std::make_unique<FileWatcher>(
|
||||
GetSettingsFileName(),
|
||||
[this]() {
|
||||
Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event.");
|
||||
SetEvent(m_settingsChangedEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void LightSwitchSettings::AddObserver(SettingsObserver& observer)
|
||||
|
||||
@@ -14,6 +14,7 @@ class SettingsObserver;
|
||||
|
||||
enum class ScheduleMode
|
||||
{
|
||||
Off,
|
||||
FixedHours,
|
||||
SunsetToSunrise
|
||||
// Add more in the future
|
||||
@@ -28,7 +29,7 @@ inline std::wstring ToString(ScheduleMode mode)
|
||||
case ScheduleMode::SunsetToSunrise:
|
||||
return L"SunsetToSunrise";
|
||||
default:
|
||||
return L"FixedHours";
|
||||
return L"Off";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +37,10 @@ inline ScheduleMode FromString(const std::wstring& str)
|
||||
{
|
||||
if (str == L"SunsetToSunrise")
|
||||
return ScheduleMode::SunsetToSunrise;
|
||||
else
|
||||
if (str == L"FixedHours")
|
||||
return ScheduleMode::FixedHours;
|
||||
else
|
||||
return ScheduleMode::Off;
|
||||
}
|
||||
|
||||
struct LightSwitchConfig
|
||||
@@ -76,6 +79,8 @@ public:
|
||||
|
||||
void LoadSettings();
|
||||
|
||||
HANDLE GetSettingsChangedEvent() const { return m_settingsChangedEvent; }
|
||||
|
||||
private:
|
||||
LightSwitchSettings();
|
||||
~LightSwitchSettings() = default;
|
||||
@@ -85,4 +90,6 @@ private:
|
||||
std::unordered_set<SettingsObserver*> m_observers;
|
||||
|
||||
void NotifyObservers(SettingId id) const;
|
||||
|
||||
HANDLE m_settingsChangedEvent = nullptr;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
#include <windows.h>
|
||||
#include <logger/logger_settings.h>
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "ThemeHelper.h"
|
||||
|
||||
// Controls changing the themes.
|
||||
|
||||
static void ResetColorPrevalence()
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
0,
|
||||
KEY_SET_VALUE,
|
||||
&hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
DWORD value = 0; // back to default value
|
||||
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void SetAppsTheme(bool mode)
|
||||
{
|
||||
HKEY hKey;
|
||||
@@ -35,6 +59,12 @@ void SetSystemTheme(bool mode)
|
||||
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
if (mode) // if are changing to light mode
|
||||
{
|
||||
ResetColorPrevalence();
|
||||
Logger::info(L"[LightSwitchService] Reset ColorPrevalence to default when switching to light mode.");
|
||||
}
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
|
||||
|
||||
@@ -269,6 +269,10 @@ 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)
|
||||
@@ -535,7 +539,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, 0);
|
||||
SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, SWP_NOACTIVATE);
|
||||
m_sonarPos = ptNowhere;
|
||||
OnMouseTimer();
|
||||
UpdateMouseSnooping();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project>
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")]
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Settings"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:text="using:Windows.UI.Text"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="SettingsWindow"
|
||||
@@ -73,14 +74,14 @@
|
||||
ItemsSource="{x:Bind BreadCrumbs, Mode=OneWay}">
|
||||
<BreadcrumbBar.ItemTemplate>
|
||||
<DataTemplate x:DataType="local:Crumb">
|
||||
<TextBlock Text="{x:Bind Label, Mode=OneWay}" />
|
||||
<TextBlock Text="{x:Bind Label}" />
|
||||
</DataTemplate>
|
||||
</BreadcrumbBar.ItemTemplate>
|
||||
<BreadcrumbBar.Resources>
|
||||
<ResourceDictionary>
|
||||
<x:Double x:Key="BreadcrumbBarItemThemeFontSize">28</x:Double>
|
||||
<Thickness x:Key="BreadcrumbBarChevronPadding">7,4,8,0</Thickness>
|
||||
<FontWeight x:Key="BreadcrumbBarItemFontWeight">SemiBold</FontWeight>
|
||||
<text:FontWeight x:Key="BreadcrumbBarItemFontWeight">SemiBold</text:FontWeight>
|
||||
<x:Double x:Key="BreadcrumbBarChevronFontSize">16</x:Double>
|
||||
</ResourceDictionary>
|
||||
</BreadcrumbBar.Resources>
|
||||
|
||||
@@ -17,6 +17,7 @@ public class Settings : ISettingsInterface
|
||||
private readonly bool hideKillProcessOnElevatedProcesses;
|
||||
private readonly bool hideExplorerSettingInfo;
|
||||
private readonly bool inMruOrder;
|
||||
private readonly bool useWindowIcon;
|
||||
|
||||
public Settings(
|
||||
bool resultsFromVisibleDesktopOnly = false,
|
||||
@@ -27,7 +28,8 @@ public class Settings : ISettingsInterface
|
||||
bool openAfterKillAndClose = false,
|
||||
bool hideKillProcessOnElevatedProcesses = false,
|
||||
bool hideExplorerSettingInfo = true,
|
||||
bool inMruOrder = true)
|
||||
bool inMruOrder = true,
|
||||
bool useWindowIcon = true)
|
||||
{
|
||||
this.resultsFromVisibleDesktopOnly = resultsFromVisibleDesktopOnly;
|
||||
this.subtitleShowPid = subtitleShowPid;
|
||||
@@ -38,6 +40,7 @@ public class Settings : ISettingsInterface
|
||||
this.hideKillProcessOnElevatedProcesses = hideKillProcessOnElevatedProcesses;
|
||||
this.hideExplorerSettingInfo = hideExplorerSettingInfo;
|
||||
this.inMruOrder = inMruOrder;
|
||||
this.useWindowIcon = useWindowIcon;
|
||||
}
|
||||
|
||||
public bool ResultsFromVisibleDesktopOnly => resultsFromVisibleDesktopOnly;
|
||||
@@ -57,4 +60,6 @@ public class Settings : ISettingsInterface
|
||||
public bool HideExplorerSettingInfo => hideExplorerSettingInfo;
|
||||
|
||||
public bool InMruOrder => inMruOrder;
|
||||
|
||||
public bool UseWindowIcon => useWindowIcon;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// 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.SysFreeStringSafeHandle")]
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
@@ -187,7 +187,7 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to .
|
||||
/// Looks up a localized string similar to Show a confirmation dialog when manually deleting an item.
|
||||
/// </summary>
|
||||
public static string settings_confirm_delete_description {
|
||||
get {
|
||||
@@ -196,7 +196,7 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show a confirmation dialog when manually deleting an item.
|
||||
/// Looks up a localized string similar to Ask for confirmation before deleting items.
|
||||
/// </summary>
|
||||
public static string settings_confirm_delete_title {
|
||||
get {
|
||||
@@ -205,7 +205,7 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to .
|
||||
/// Looks up a localized string similar to Keep items in clipboard history after pasting.
|
||||
/// </summary>
|
||||
public static string settings_keep_after_paste_description {
|
||||
get {
|
||||
@@ -214,7 +214,7 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Keep items in clipboard history after pasting.
|
||||
/// Looks up a localized string similar to Keep items after pasting.
|
||||
/// </summary>
|
||||
public static string settings_keep_after_paste_title {
|
||||
get {
|
||||
|
||||
@@ -151,16 +151,16 @@
|
||||
<value>Deleted from clipboard history</value>
|
||||
</data>
|
||||
<data name="settings_keep_after_paste_description" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="settings_keep_after_paste_title" xml:space="preserve">
|
||||
<value>Keep items in clipboard history after pasting</value>
|
||||
</data>
|
||||
<data name="settings_keep_after_paste_title" xml:space="preserve">
|
||||
<value>Keep items after pasting</value>
|
||||
</data>
|
||||
<data name="settings_confirm_delete_title" xml:space="preserve">
|
||||
<value>Show a confirmation dialog when manually deleting an item</value>
|
||||
<value>Ask for confirmation before deleting items</value>
|
||||
</data>
|
||||
<data name="settings_confirm_delete_description" xml:space="preserve">
|
||||
<value />
|
||||
<value>Show a confirmation dialog when manually deleting an item</value>
|
||||
</data>
|
||||
<data name="delete_confirmation_title" xml:space="preserve">
|
||||
<value>Delete item?</value>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// 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.LocalFreeSafeHandle")]
|
||||
@@ -27,6 +27,14 @@ internal sealed class NetworkConnectionProperties
|
||||
/// <seealso cref="https://github.com/xoofx/markdig/blob/master/src/Markdig/Extensions/Emoji/EmojiMapping.cs"/>
|
||||
private const int GreenCircleCharacter = 128994;
|
||||
|
||||
/// <summary>
|
||||
/// Decimal unicode value for green circle emoji.
|
||||
/// We need to generate it in the code because it does not render using Markdown emoji syntax or Unicode character syntax.
|
||||
/// </summary>
|
||||
/// <seealso cref="https://github.com/CommunityToolkit/Labs-Windows/blob/main/components/MarkdownTextBlock/samples/MarkdownTextBlock.md"/>
|
||||
/// <seealso cref="https://github.com/xoofx/markdig/blob/master/src/Markdig/Extensions/Emoji/EmojiMapping.cs"/>
|
||||
private const int RedCircleCharacter = 128308;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the adapter
|
||||
/// </summary>
|
||||
@@ -170,7 +178,7 @@ internal sealed class NetworkConnectionProperties
|
||||
internal string GetAdapterDetails()
|
||||
{
|
||||
return $"**{Resources.Microsoft_plugin_sys_AdapterName}:** {Adapter}" +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_State}:** " + (State == OperationalStatus.Up ? char.ConvertFromUtf32(GreenCircleCharacter) + " " + Resources.Microsoft_plugin_sys_Connected : ":red_circle: " + Resources.Microsoft_plugin_sys_Disconnected) +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_State}:** " + (State == OperationalStatus.Up ? char.ConvertFromUtf32(GreenCircleCharacter) + " " + Resources.Microsoft_plugin_sys_Connected : char.ConvertFromUtf32(RedCircleCharacter) + " " + Resources.Microsoft_plugin_sys_Disconnected) +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_PhysicalAddress}:** {PhysicalAddress}" +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_Speed}:** {GetFormattedSpeedValue(Speed)}" +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_Type}:** {GetAdapterTypeAsString(Type)}" +
|
||||
@@ -184,7 +192,7 @@ internal sealed class NetworkConnectionProperties
|
||||
internal string GetConnectionDetails()
|
||||
{
|
||||
return $"**{Resources.Microsoft_plugin_sys_ConnectionName}:** {ConnectionName}" +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_State}:** " + (State == OperationalStatus.Up ? char.ConvertFromUtf32(GreenCircleCharacter) + " " + Resources.Microsoft_plugin_sys_Connected : ":red_circle: " + Resources.Microsoft_plugin_sys_Disconnected) +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_State}:** " + (State == OperationalStatus.Up ? char.ConvertFromUtf32(GreenCircleCharacter) + " " + Resources.Microsoft_plugin_sys_Connected : char.ConvertFromUtf32(RedCircleCharacter) + " " + Resources.Microsoft_plugin_sys_Disconnected) +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_Type}:** {GetAdapterTypeAsString(Type)}" +
|
||||
$"\n\n**{Resources.Microsoft_plugin_sys_Suffix}:** {Suffix}" +
|
||||
CreateIpInfoForDetailsText($"**{Resources.Microsoft_plugin_sys_Ip4Address}:** ", IPv4) +
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.WindowWalker.Components;
|
||||
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WindowWalker.Commands;
|
||||
|
||||
@@ -16,20 +21,53 @@ internal sealed partial class SwitchToWindowCommand : InvokableCommand
|
||||
|
||||
public SwitchToWindowCommand(Window? window)
|
||||
{
|
||||
Icon = Icons.GenericAppIcon; // Fallback to default icon
|
||||
Name = Resources.switch_to_command_title;
|
||||
_window = window;
|
||||
if (_window is not null)
|
||||
{
|
||||
var p = Process.GetProcessById((int)_window.Process.ProcessID);
|
||||
if (p is not null)
|
||||
// Use window icon
|
||||
if (SettingsManager.Instance.UseWindowIcon)
|
||||
{
|
||||
try
|
||||
if (_window.TryGetWindowIcon(out var icon) && icon is not null)
|
||||
{
|
||||
var processFileName = p.MainModule?.FileName;
|
||||
Icon = new IconInfo(processFileName);
|
||||
try
|
||||
{
|
||||
using var bitmap = icon.ToBitmap();
|
||||
using var memoryStream = new MemoryStream();
|
||||
bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
|
||||
var raStream = new InMemoryRandomAccessStream();
|
||||
using var outputStream = raStream.GetOutputStreamAt(0);
|
||||
using var dataWriter = new DataWriter(outputStream);
|
||||
dataWriter.WriteBytes(memoryStream.ToArray());
|
||||
dataWriter.StoreAsync().AsTask().Wait();
|
||||
dataWriter.FlushAsync().AsTask().Wait();
|
||||
Icon = IconInfo.FromStream(raStream);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
icon.Dispose();
|
||||
}
|
||||
}
|
||||
catch
|
||||
}
|
||||
|
||||
// Use process icon
|
||||
else
|
||||
{
|
||||
var p = Process.GetProcessById((int)_window.Process.ProcessID);
|
||||
if (p is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processFileName = p.MainModule?.FileName;
|
||||
Icon = new IconInfo(processFileName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,62 @@ internal sealed class Window
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the window icon.
|
||||
/// </summary>
|
||||
/// <param name="icon">The window icon if found; otherwise, null.</param>
|
||||
/// <returns>True if an icon was found; otherwise, false.</returns>
|
||||
internal bool TryGetWindowIcon(out System.Drawing.Icon? icon)
|
||||
{
|
||||
icon = null;
|
||||
|
||||
if (hwnd == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try WM_GETICON with SendMessageTimeout
|
||||
if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_BIG, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out var result) != 0 && result != 0)
|
||||
{
|
||||
icon = System.Drawing.Icon.FromHandle((IntPtr)result);
|
||||
NativeMethods.DestroyIcon((IntPtr)result);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_SMALL, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out result) != 0 && result != 0)
|
||||
{
|
||||
icon = System.Drawing.Icon.FromHandle((IntPtr)result);
|
||||
NativeMethods.DestroyIcon((IntPtr)result);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_SMALL2, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out result) != 0 && result != 0)
|
||||
{
|
||||
icon = System.Drawing.Icon.FromHandle((IntPtr)result);
|
||||
NativeMethods.DestroyIcon((IntPtr)result);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to GetClassLongPtr
|
||||
var iconHandle = NativeMethods.GetClassLongPtr(hwnd, Win32Constants.GCLP_HICON);
|
||||
if (iconHandle != IntPtr.Zero)
|
||||
{
|
||||
icon = System.Drawing.Icon.FromHandle(iconHandle);
|
||||
NativeMethods.DestroyIcon((IntPtr)iconHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
iconHandle = NativeMethods.GetClassLongPtr(hwnd, Win32Constants.GCLP_HICONSM);
|
||||
if (iconHandle != IntPtr.Zero)
|
||||
{
|
||||
icon = System.Drawing.Icon.FromHandle(iconHandle);
|
||||
NativeMethods.DestroyIcon((IntPtr)iconHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the window name to string along with the process name
|
||||
/// </summary>
|
||||
|
||||
@@ -23,4 +23,6 @@ public interface ISettingsInterface
|
||||
public bool HideExplorerSettingInfo { get; }
|
||||
|
||||
public bool InMruOrder { get; }
|
||||
|
||||
public bool UseWindowIcon { get; }
|
||||
}
|
||||
|
||||
@@ -84,6 +84,12 @@ public static partial class NativeMethods
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int SendMessageTimeout(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam, int fuFlags, int uTimeout, out int lpdwResult);
|
||||
|
||||
[DllImport("user32.dll", EntryPoint = "GetClassLongPtr")]
|
||||
public static extern IntPtr GetClassLongPtr(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern bool DestroyIcon(IntPtr hIcon);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CloseHandle(IntPtr hObject);
|
||||
@@ -143,6 +149,41 @@ public static class Win32Constants
|
||||
/// </summary>
|
||||
public const int SC_CLOSE = 0xF060;
|
||||
|
||||
/// <summary>
|
||||
/// Sent to a window to retrieve a handle to the large or small icon associated with a window.
|
||||
/// </summary>
|
||||
public const uint WM_GETICON = 0x007F;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the large icon for the window.
|
||||
/// </summary>
|
||||
public const int ICON_BIG = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the small icon for the window.
|
||||
/// </summary>
|
||||
public const int ICON_SMALL = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the small icon provided by the application.
|
||||
/// </summary>
|
||||
public const int ICON_SMALL2 = 2;
|
||||
|
||||
/// <summary>
|
||||
/// The function returns if the receiving thread does not respond within the timeout period.
|
||||
/// </summary>
|
||||
public const int SMTO_ABORTIFHUNG = 0x0002;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a handle to the icon associated with the class.
|
||||
/// </summary>
|
||||
public const int GCLP_HICON = -14;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a handle to the small icon associated with the class.
|
||||
/// </summary>
|
||||
public const int GCLP_HICONSM = -34;
|
||||
|
||||
/// <summary>
|
||||
/// RPC call succeeded
|
||||
/// </summary>
|
||||
|
||||
@@ -70,6 +70,12 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Resources.windowwalker_SettingInMruOrder_Description,
|
||||
true);
|
||||
|
||||
private readonly ToggleSetting _useWindowIcon = new(
|
||||
Namespaced(nameof(UseWindowIcon)),
|
||||
Resources.windowwalker_SettingUseWindowIcon,
|
||||
Resources.windowwalker_SettingUseWindowIcon_Description,
|
||||
true);
|
||||
|
||||
public bool ResultsFromVisibleDesktopOnly => _resultsFromVisibleDesktopOnly.Value;
|
||||
|
||||
public bool SubtitleShowPid => _subtitleShowPid.Value;
|
||||
@@ -88,6 +94,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
public bool InMruOrder => _inMruOrder.Value;
|
||||
|
||||
public bool UseWindowIcon => _useWindowIcon.Value;
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
@@ -110,6 +118,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Settings.Add(_hideKillProcessOnElevatedProcesses);
|
||||
Settings.Add(_hideExplorerSettingInfo);
|
||||
Settings.Add(_inMruOrder);
|
||||
Settings.Add(_useWindowIcon);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
@@ -15,4 +15,6 @@ internal sealed class Icons
|
||||
internal static IconInfo CloseWindow { get; } = new IconInfo("\uE894"); // Clear
|
||||
|
||||
internal static IconInfo Info { get; } = new IconInfo("\uE946"); // Info
|
||||
|
||||
internal static IconInfo GenericAppIcon { get; } = new("\uE737"); // Favicon
|
||||
}
|
||||
|
||||
@@ -401,5 +401,23 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
|
||||
return ResourceManager.GetString("windowwalker_SettingTagPid", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use window icons.
|
||||
/// </summary>
|
||||
public static string windowwalker_SettingUseWindowIcon {
|
||||
get {
|
||||
return ResourceManager.GetString("windowwalker_SettingUseWindowIcon", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show the actual window icon instead of the process icon.
|
||||
/// </summary>
|
||||
public static string windowwalker_SettingUseWindowIcon_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("windowwalker_SettingUseWindowIcon_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,4 +235,10 @@
|
||||
<data name="windowwalker_NoResultsMessage" xml:space="preserve">
|
||||
<value>No open windows found</value>
|
||||
</data>
|
||||
<data name="windowwalker_SettingUseWindowIcon" xml:space="preserve">
|
||||
<value>Use window icons</value>
|
||||
</data>
|
||||
<data name="windowwalker_SettingUseWindowIcon_Description" xml:space="preserve">
|
||||
<value>Show the actual window icon instead of the process icon</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -24,6 +24,14 @@
|
||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
@@ -55,4 +63,14 @@
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Ensure Resources directory and ImageResizer.png are available for dependent projects -->
|
||||
<Target Name="CopyResourcesToSharedLocation" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<ResourceFiles Include="$(MSBuildProjectDirectory)\Resources\ImageResizer.png" />
|
||||
</ItemGroup>
|
||||
<MakeDir Directories="$(OutputPath)Resources" Condition="!Exists('$(OutputPath)Resources')" />
|
||||
<Copy SourceFiles="@(ResourceFiles)" DestinationFolder="$(OutputPath)Resources" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
8
src/modules/imageresizer/ui/ImageResizerUI.dev.manifest
Normal file
8
src/modules/imageresizer/ui/ImageResizerUI.dev.manifest
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.ImageResizer.app" />
|
||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
||||
publisher="CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US"
|
||||
packageName="Microsoft.PowerToys.SparseApp"
|
||||
applicationId="PowerToys.ImageResizerUI" />
|
||||
</assembly>
|
||||
8
src/modules/imageresizer/ui/ImageResizerUI.prod.manifest
Normal file
8
src/modules/imageresizer/ui/ImageResizerUI.prod.manifest
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.ImageResizer.app" />
|
||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
||||
publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
packageName="Microsoft.PowerToys.SparseApp"
|
||||
applicationId="PowerToys.ImageResizerUI" />
|
||||
</assembly>
|
||||
@@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public const int DefaultSunsetOffset = 0;
|
||||
public const string DefaultLatitude = "0.0";
|
||||
public const string DefaultLongitude = "0.0";
|
||||
public const string DefaultScheduleMode = "FixedHours";
|
||||
public const string DefaultScheduleMode = "Off";
|
||||
public static readonly HotkeySettings DefaultToggleThemeHotkey = new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Win+Shift+D
|
||||
|
||||
public LightSwitchProperties()
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Converters
|
||||
{
|
||||
public partial class EnumToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value == null || parameter == null)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
string enumString = value.ToString();
|
||||
string targetString = parameter.ToString();
|
||||
|
||||
return enumString.Equals(targetString, StringComparison.OrdinalIgnoreCase)
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,143 +17,175 @@
|
||||
AutomationProperties.LandmarkType="Main"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<converters:EnumToVisibilityConverter x:Key="EnumToVisibilityConverter" />
|
||||
<converters:TimeSpanToFriendlyTimeConverter x:Key="TimeSpanToFriendlyTimeConverter" />
|
||||
</Page.Resources>
|
||||
<controls:SettingsPageControl
|
||||
x:Uid="LightSwitch"
|
||||
IsTabStop="False"
|
||||
ModuleImageSource="ms-appx:///Assets/Settings/Modules/LightSwitch.png">
|
||||
<controls:SettingsPageControl.ModuleContent>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="LightSwitch_EnableSettingsCard"
|
||||
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}"
|
||||
IsEnabled="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="Toggle_LightSwitch" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:GPOInfoControl>
|
||||
<Grid>
|
||||
<controls:SettingsPageControl
|
||||
x:Uid="LightSwitch"
|
||||
IsTabStop="False"
|
||||
ModuleImageSource="ms-appx:///Assets/Settings/Modules/LightSwitch.png">
|
||||
<controls:SettingsPageControl.ModuleContent>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="LightSwitch_EnableSettingsCard"
|
||||
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}"
|
||||
IsEnabled="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="Toggle_LightSwitch" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:GPOInfoControl>
|
||||
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_ShortcutsSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_ThemeToggle_Shortcut" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
AutomationProperties.AutomationId="Shortcut_LightSwitch"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.ToggleThemeActivationShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_ShortcutsSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_ThemeToggle_Shortcut" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AllowDisable="True"
|
||||
AutomationProperties.AutomationId="Shortcut_LightSwitch"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.ToggleThemeActivationShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_ScheduleSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="LightSwitch_ModeSettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ComboBox
|
||||
x:Name="ModeSelector"
|
||||
AutomationProperties.AutomationId="ModeSelection_LightSwitch"
|
||||
SelectedValue="{x:Bind ViewModel.ScheduleMode, Mode=TwoWay}"
|
||||
SelectedValuePath="Tag"
|
||||
SelectionChanged="ModeSelector_SelectionChanged">
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeManual"
|
||||
AutomationProperties.AutomationId="ManualCBItem_LightSwitch"
|
||||
Tag="FixedHours" />
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeSunsetToSunrise"
|
||||
AutomationProperties.AutomationId="SunCBItem_LightSwitch"
|
||||
Tag="SunsetToSunrise" />
|
||||
</ComboBox>
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_TurnOnDarkMode" Visibility="{x:Bind ViewModel.ScheduleMode, Mode=OneWay, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter=FixedHours}">
|
||||
<TimePicker AutomationProperties.AutomationId="DarkTimePicker" Time="{x:Bind ViewModel.DarkTimePickerValue, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_TurnOffDarkMode" Visibility="{x:Bind ViewModel.ScheduleMode, Mode=OneWay, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter=FixedHours}">
|
||||
<TimePicker AutomationProperties.AutomationId="LightTimePicker" Time="{x:Bind ViewModel.LightTimePickerValue, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_LocationSettingsCard" Visibility="{x:Bind ViewModel.ScheduleMode, Mode=OneWay, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter=SunsetToSunrise}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.SyncButtonInformation, Mode=OneWay}" />
|
||||
<Button
|
||||
Padding="8"
|
||||
AutomationProperties.AutomationId="SetLocationButton_LightSwitch"
|
||||
Click="SyncLocationButton_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard x:Uid="LightSwitch_OffsetSettingsCard" Visibility="{x:Bind ViewModel.ScheduleMode, Mode=OneWay, Converter={StaticResource EnumToVisibilityConverter}, ConverterParameter=SunsetToSunrise}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="20">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<!--<FontIcon Glyph="" FontSize="16" />-->
|
||||
<controls:IsEnabledTextBlock x:Uid="LightSwitch_SunriseText" VerticalAlignment="Center" />
|
||||
<NumberBox
|
||||
AutomationProperties.AutomationId="SunriseOffset_LightSwitch"
|
||||
Maximum="60"
|
||||
Minimum="-60"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.SunriseOffset, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<controls:IsEnabledTextBlock x:Uid="LightSwitch_SunsetText" VerticalAlignment="Center" />
|
||||
<NumberBox
|
||||
AutomationProperties.AutomationId="SunsetOffset_LightSwitch"
|
||||
Maximum="60"
|
||||
Minimum="-60"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.SunsetOffset, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="TimelineCard"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical">
|
||||
<controls:Timeline
|
||||
Margin="0,24,0,24"
|
||||
AutomationProperties.AutomationId="Timeline_LightSwitch"
|
||||
EndTime="{x:Bind ViewModel.DarkTimeTimeSpan, Mode=OneWay}"
|
||||
StartTime="{x:Bind ViewModel.LightTimeTimeSpan, Mode=OneWay}"
|
||||
Sunrise="{x:Bind ViewModel.SunriseTimeSpan, Mode=OneWay}"
|
||||
Sunset="{x:Bind ViewModel.SunsetTimeSpan, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
<InfoBar
|
||||
x:Name="LocationWarningBar"
|
||||
x:Uid="LightSwitch_LocationWarningBar"
|
||||
IsOpen="True"
|
||||
Severity="Informational"
|
||||
Visibility="Collapsed" />
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_ScheduleSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="LightSwitch_ApplyDarkModeExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
x:Uid="LightSwitch_ModeSettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ComboBox
|
||||
x:Name="ModeSelector"
|
||||
AutomationProperties.AutomationId="ModeSelection_LightSwitch"
|
||||
SelectedValue="{x:Bind ViewModel.ScheduleMode, Mode=TwoWay}"
|
||||
SelectedValuePath="Tag"
|
||||
SelectionChanged="ModeSelector_SelectionChanged">
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeOff"
|
||||
AutomationProperties.AutomationId="OffCBItem_LightSwitch"
|
||||
Tag="Off" />
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeManual"
|
||||
AutomationProperties.AutomationId="ManualCBItem_LightSwitch"
|
||||
Tag="FixedHours" />
|
||||
<ComboBoxItem
|
||||
x:Uid="LightSwitch_ModeSunsetToSunrise"
|
||||
AutomationProperties.AutomationId="SunCBItem_LightSwitch"
|
||||
Tag="SunsetToSunrise" />
|
||||
</ComboBox>
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
|
||||
<controls:CheckBoxWithDescriptionControl
|
||||
x:Uid="LightSwitch_SystemCheckbox"
|
||||
AutomationProperties.AutomationId="ChangeSystemCheckbox_LightSwitch"
|
||||
IsChecked="{x:Bind ViewModel.ChangeSystem, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="Fixed_TurnOnCard"
|
||||
x:Uid="LightSwitch_TurnOnDarkMode"
|
||||
Visibility="Collapsed">
|
||||
<TimePicker AutomationProperties.AutomationId="DarkTimePicker" Time="{x:Bind ViewModel.DarkTimePickerValue, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
|
||||
<controls:CheckBoxWithDescriptionControl
|
||||
x:Uid="LightSwitch_AppsCheckbox"
|
||||
AutomationProperties.AutomationId="ChangeAppsCheckbox_LightSwitch"
|
||||
IsChecked="{x:Bind ViewModel.ChangeApps, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="Fixed_TurnOffCard"
|
||||
x:Uid="LightSwitch_TurnOffDarkMode"
|
||||
Visibility="Collapsed">
|
||||
<TimePicker AutomationProperties.AutomationId="LightTimePicker" Time="{x:Bind ViewModel.LightTimePickerValue, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="SunLocation_Card"
|
||||
x:Uid="LightSwitch_LocationSettingsCard"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.SyncButtonInformation, Mode=OneWay}" />
|
||||
<Button
|
||||
Padding="8"
|
||||
AutomationProperties.AutomationId="SetLocationButton_LightSwitch"
|
||||
Click="SyncLocationButton_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="SunOffset_Card"
|
||||
x:Uid="LightSwitch_OffsetSettingsCard"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="20">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<!--<FontIcon Glyph="" FontSize="16" />-->
|
||||
<controls:IsEnabledTextBlock x:Uid="LightSwitch_SunriseText" VerticalAlignment="Center" />
|
||||
<NumberBox
|
||||
AutomationProperties.AutomationId="SunriseOffset_LightSwitch"
|
||||
Maximum="60"
|
||||
Minimum="-60"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.SunriseOffset, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<controls:IsEnabledTextBlock x:Uid="LightSwitch_SunsetText" VerticalAlignment="Center" />
|
||||
<NumberBox
|
||||
AutomationProperties.AutomationId="SunsetOffset_LightSwitch"
|
||||
Maximum="60"
|
||||
Minimum="-60"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
Value="{x:Bind ViewModel.SunsetOffset, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="TimelineCard"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
Visibility="Collapsed">
|
||||
<controls:Timeline
|
||||
Margin="0,24,0,24"
|
||||
AutomationProperties.AutomationId="Timeline_LightSwitch"
|
||||
EndTime="{x:Bind ViewModel.DarkTimeTimeSpan, Mode=OneWay}"
|
||||
StartTime="{x:Bind ViewModel.LightTimeTimeSpan, Mode=OneWay}"
|
||||
Sunrise="{x:Bind ViewModel.SunriseTimeSpan, Mode=OneWay}"
|
||||
Sunset="{x:Bind ViewModel.SunsetTimeSpan, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="NoScheduleCard"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}"
|
||||
ContentAlignment="Vertical"
|
||||
Visibility="Visible">
|
||||
<InfoBar
|
||||
x:Uid="LightSwitch_ScheduleOffMessage"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Severity="Informational" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
<!-- Force mode buttons -->
|
||||
<!--<tkcontrols:SettingsCard
|
||||
<InfoBar
|
||||
x:Name="LocationWarningBar"
|
||||
x:Uid="LightSwitch_LocationWarningBar"
|
||||
IsOpen="True"
|
||||
Severity="Informational"
|
||||
Visibility="Collapsed" />
|
||||
<controls:SettingsGroup x:Uid="LightSwitch_BehaviorSettingsGroup" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsExpander
|
||||
x:Uid="LightSwitch_ApplyDarkModeExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
|
||||
<controls:CheckBoxWithDescriptionControl
|
||||
x:Uid="LightSwitch_SystemCheckbox"
|
||||
AutomationProperties.AutomationId="ChangeSystemCheckbox_LightSwitch"
|
||||
IsChecked="{x:Bind ViewModel.ChangeSystem, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
|
||||
<controls:CheckBoxWithDescriptionControl
|
||||
x:Uid="LightSwitch_AppsCheckbox"
|
||||
AutomationProperties.AutomationId="ChangeAppsCheckbox_LightSwitch"
|
||||
IsChecked="{x:Bind ViewModel.ChangeApps, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
<!-- Force mode buttons -->
|
||||
<!--<tkcontrols:SettingsCard
|
||||
Header="Force mode now"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
Description="Apply light or dark mode immediately">
|
||||
@@ -167,21 +199,21 @@
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>-->
|
||||
|
||||
<ContentDialog
|
||||
x:Name="LocationDialog"
|
||||
x:Uid="LightSwitch_LocationDialog"
|
||||
IsPrimaryButtonEnabled="True"
|
||||
IsSecondaryButtonEnabled="True"
|
||||
Opened="LocationDialog_Opened"
|
||||
PrimaryButtonClick="LocationDialog_PrimaryButtonClick"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<Grid RowSpacing="48">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock x:Uid="LightSwitch_LocationDialog_Description" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<!--<AutoSuggestBox
|
||||
<ContentDialog
|
||||
x:Name="LocationDialog"
|
||||
x:Uid="LightSwitch_LocationDialog"
|
||||
IsPrimaryButtonEnabled="True"
|
||||
IsSecondaryButtonEnabled="True"
|
||||
Opened="LocationDialog_Opened"
|
||||
PrimaryButtonClick="LocationDialog_PrimaryButtonClick"
|
||||
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
|
||||
<Grid RowSpacing="48">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock x:Uid="LightSwitch_LocationDialog_Description" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<!--<AutoSuggestBox
|
||||
x:Name="CityAutoSuggestBox"
|
||||
Grid.Row="1"
|
||||
Margin="0,16,0,8"
|
||||
@@ -208,113 +240,122 @@
|
||||
</DataTemplate>
|
||||
</AutoSuggestBox.ItemTemplate>
|
||||
</AutoSuggestBox>-->
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,24,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="32">
|
||||
<Button
|
||||
x:Name="SyncButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.AutomationId="SyncLocationButton_LightSwitch"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="LightSwitch_GetCurrentLocation" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<ProgressRing
|
||||
x:Name="SyncLoader"
|
||||
Grid.Row="1"
|
||||
Width="40"
|
||||
Height="40"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="False"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<Grid
|
||||
x:Name="LocationResultPanel"
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Bottom"
|
||||
ColumnSpacing="16"
|
||||
RowSpacing="12"
|
||||
Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<FontIcon FontSize="16" Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_LocationTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
AutomationProperties.AutomationId="LocationResultText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextAlignment="Center">
|
||||
<Run Text="{x:Bind ViewModel.Latitude, Mode=OneWay}" /><Run Text="°, " />
|
||||
<Run Text="{x:Bind ViewModel.Longitude, Mode=OneWay}" /><Run Text="°" />
|
||||
</TextBlock>
|
||||
<FontIcon
|
||||
Grid.Column="1"
|
||||
FontSize="20"
|
||||
Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_SunriseTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.AutomationId="SunriseText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.LightTimeTimeSpan, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Center" />
|
||||
<FontIcon
|
||||
Grid.Column="2"
|
||||
FontSize="20"
|
||||
Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_SunsetTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.AutomationId="SunsetText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.DarkTimeTimeSpan, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
</controls:SettingsGroup>
|
||||
Margin="0,24,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="32">
|
||||
<Button
|
||||
x:Name="SyncButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.AutomationId="SyncLocationButton_LightSwitch"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="LightSwitch_GetCurrentLocation" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
<controls:SettingsPageControl.PrimaryLinks>
|
||||
<controls:PageLink x:Uid="LearnMore_LightSwitch" Link="https://aka.ms/PowerToysOverview_LightSwitch" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="LocationEnabledStates">
|
||||
<VisualState x:Name="LocationSet" />
|
||||
<VisualState x:Name="LocationNotSet">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TimelineCard.Visibility" Value="Collapsed" />
|
||||
<Setter Target="LocationWarningBar.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
<ProgressRing
|
||||
x:Name="SyncLoader"
|
||||
Grid.Row="1"
|
||||
Width="40"
|
||||
Height="40"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="False"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<Grid
|
||||
x:Name="LocationResultPanel"
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Bottom"
|
||||
ColumnSpacing="16"
|
||||
RowSpacing="12"
|
||||
Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<FontIcon FontSize="16" Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_LocationTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
AutomationProperties.AutomationId="LocationResultText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextAlignment="Center">
|
||||
<Run Text="{x:Bind ViewModel.Latitude, Mode=OneWay}" /><Run Text="°, " />
|
||||
<Run Text="{x:Bind ViewModel.Longitude, Mode=OneWay}" /><Run Text="°" />
|
||||
</TextBlock>
|
||||
<FontIcon
|
||||
Grid.Column="1"
|
||||
FontSize="20"
|
||||
Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_SunriseTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.AutomationId="SunriseText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.LightTimeTimeSpan, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Center" />
|
||||
<FontIcon
|
||||
Grid.Column="2"
|
||||
FontSize="20"
|
||||
Glyph="">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="LightSwitch_SunsetTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</FontIcon>
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.AutomationId="SunsetText_LightSwitch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.DarkTimeTimeSpan, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}"
|
||||
TextAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
<controls:SettingsPageControl.PrimaryLinks>
|
||||
<controls:PageLink x:Uid="LearnMore_LightSwitch" Link="https://aka.ms/PowerToysOverview_LightSwitch" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="ScheduleModeStates">
|
||||
<VisualState x:Name="OffState" />
|
||||
<VisualState x:Name="SunsetToSunriseState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SunLocation_Card.Visibility" Value="Visible" />
|
||||
<Setter Target="SunOffset_Card.Visibility" Value="Visible" />
|
||||
<Setter Target="NoScheduleCard.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="ManualState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="Fixed_TurnOnCard.Visibility" Value="Visible" />
|
||||
<Setter Target="Fixed_TurnOffCard.Visibility" Value="Visible" />
|
||||
<Setter Target="NoScheduleCard.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -300,20 +300,20 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private void ModeSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
SunriseModeChartState();
|
||||
}
|
||||
|
||||
private void SunriseModeChartState()
|
||||
{
|
||||
if (ViewModel.Latitude == "0.0" && ViewModel.Longitude == "0.0" && ViewModel.ScheduleMode == "SunsetToSunrise")
|
||||
switch (ViewModel.ScheduleMode)
|
||||
{
|
||||
TimelineCard.Visibility = Visibility.Collapsed;
|
||||
LocationWarningBar.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
TimelineCard.Visibility = Visibility.Visible;
|
||||
LocationWarningBar.Visibility = Visibility.Collapsed;
|
||||
case "FixedHours":
|
||||
VisualStateManager.GoToState(this, "ManualState", true);
|
||||
TimelineCard.Visibility = Visibility.Visible;
|
||||
break;
|
||||
case "SunsetToSunrise":
|
||||
VisualStateManager.GoToState(this, "SunsetToSunriseState", true);
|
||||
SunriseModeChartState();
|
||||
break;
|
||||
default:
|
||||
VisualStateManager.GoToState(this, "OffState", true);
|
||||
TimelineCard.Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,5 +321,19 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
await GetGeoLocation();
|
||||
}
|
||||
|
||||
private void SunriseModeChartState()
|
||||
{
|
||||
if (ViewModel.Latitude != "0.0" && ViewModel.Longitude != "0.0")
|
||||
{
|
||||
TimelineCard.Visibility = Visibility.Visible;
|
||||
LocationWarningBar.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
TimelineCard.Visibility = Visibility.Collapsed;
|
||||
LocationWarningBar.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5269,12 +5269,18 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="LightSwitch_ModeSettingsExpander.Description" xml:space="preserve">
|
||||
<value>Determine when dark mode should be turned on</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ModeOff.Content" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ModeManual.Content" xml:space="preserve">
|
||||
<value>Manual</value>
|
||||
<value>Fixed hours</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ModeSunsetToSunrise.Content" xml:space="preserve">
|
||||
<value>Sunset to sunrise</value>
|
||||
</data>
|
||||
<data name="LightSwitch_ScheduleOffMessage.Title" xml:space="preserve">
|
||||
<value>Scheduling is turned off.</value>
|
||||
</data>
|
||||
<data name="LightSwitch_TurnOnDarkMode.Header" xml:space="preserve">
|
||||
<value>Turn on dark mode</value>
|
||||
</data>
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
AvailableScheduleModes = new ObservableCollection<string>
|
||||
{
|
||||
"Off",
|
||||
"FixedHours",
|
||||
"SunsetToSunrise",
|
||||
};
|
||||
|
||||
@@ -19,13 +19,13 @@ void XmlDocumentEx::Print(winrt::Windows::Data::Xml::Dom::IXmlNode node, int ind
|
||||
PrintTagWithAttributes(node);
|
||||
if (!node.HasChildNodes())
|
||||
{
|
||||
stream << L"<\\" << node.NodeName().c_str() << ">" << std::endl;
|
||||
stream << L"</" << node.NodeName().c_str() << ">" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.ChildNodes().Size() == 1 && !node.FirstChild().HasChildNodes())
|
||||
{
|
||||
stream << node.InnerText().c_str() << L"<\\" << node.NodeName().c_str() << ">" << std::endl;
|
||||
stream << node.InnerText().c_str() << L"</" << node.NodeName().c_str() << ">" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ void XmlDocumentEx::Print(winrt::Windows::Data::Xml::Dom::IXmlNode node, int ind
|
||||
{
|
||||
stream << " ";
|
||||
}
|
||||
stream << L"<\\" << node.NodeName().c_str() << ">" << std::endl;
|
||||
stream << L"</" << node.NodeName().c_str() << ">" << std::endl;
|
||||
}
|
||||
|
||||
void XmlDocumentEx::PrintTagWithAttributes(winrt::Windows::Data::Xml::Dom::IXmlNode node)
|
||||
|
||||
Reference in New Issue
Block a user