Compare commits

..

10 Commits

Author SHA1 Message Date
vanzue
d015b96eea POC for Always On Top window transparency 2026-01-14 10:00:04 +08:00
Kai Tao
fd88fa18d4 Fancyzones: Fix a custom layout not work in fancyzone and powertoys extension (#44661)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
1. Fix a issue that fancyzone custom layouts be able to work
2. Fix monitor info build and the icon render

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
<img width="1172" height="701" alt="image"
src="https://github.com/user-attachments/assets/0cfa71d9-8ce2-4d27-8995-c797f40f927f"
/>
2026-01-12 09:23:40 +08:00
Jaylyn Barbee
a6b8cea7cd GPO configured correctly and tested (#44567)
Ensure that Light Switch respects GPO configuration. 
In "Not configured" --> No message, everything is under the user's
control
In "Enabled" --> Module is forced enabled, policy message shows, able to
change settings
In "Disabled" --> Module is forced disabled, policy message shows,
unable to change settings
2026-01-09 10:29:17 -05:00
Jaylyn Barbee
5f61057b38 Adding a quick access button for Light Switch (#44640)
Adds a button for Light Switch in the Quick Access section of the
Dashboard page. Clicking the button will toggle the theme.
<img width="1886" height="1173" alt="image"
src="https://github.com/user-attachments/assets/7923e1ac-aeea-47ab-b648-2400cb6f3ca4"
/>
2026-01-09 10:28:54 -05:00
Shawn Yuan
0314a709f5 Optimize the module list logic (#44628)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request refactors how module enable/disable state changes are
handled in the settings UI, centralizing and simplifying the logic for
updating and notifying these changes. The main improvements include
unifying the callback mechanism for enable state changes, reducing code
duplication, and making the update process more robust against circular
updates.

**Refactoring and Simplification of Enable State Handling:**

* Introduced a unified `EnabledChangedCallback` and `UpdateStatus`
method in the base `ModuleListItem` class to handle enable/disable state
changes and notifications, replacing redundant logic in derived classes
like `FlyoutMenuItem` and `DashboardListItem`.
[[1]](diffhunk://#diff-23ab2cc13a1098a6071b3e12ce0919b7eba451d7683f6f62e5ec2cf661778a4cR21-R36)
[[2]](diffhunk://#diff-23ab2cc13a1098a6071b3e12ce0919b7eba451d7683f6f62e5ec2cf661778a4cR99-R103)
[[3]](diffhunk://#diff-5033dabc0e3ec7d01509b9d58878b9ee5745378d5e3a7fa92779bf9c111bcffcL25-L39)
[[4]](diffhunk://#diff-9c93f68ee87a48d8affd140224601da95d7fe9642ad24350c7527d0f5773ec7dL29-L43)
* Updated all usages to call `UpdateStatus` instead of directly setting
`IsEnabled`, ensuring callbacks are managed consistently and preventing
unnecessary notifications during programmatic updates.
[[1]](diffhunk://#diff-734ba1b4b3044eb540bba08334bd141c968a113625be2d92c831f3cc3debc62fL108-R109)
[[2]](diffhunk://#diff-aea3404667e7a3de2750bf9ab7ee8ff5e717892caa68ee1de86713cf8e21b44cL261-R260)

**Improvements to Callback and Update Logic:**

* Changed the signature of UI event handlers to use the base
`ModuleListItem` type, improving code maintainability and reducing
casting and duplication.
[[1]](diffhunk://#diff-734ba1b4b3044eb540bba08334bd141c968a113625be2d92c831f3cc3debc62fL160-R161)
[[2]](diffhunk://#diff-aea3404667e7a3de2750bf9ab7ee8ff5e717892caa68ee1de86713cf8e21b44cL278-R287)
* Removed obsolete or redundant flags and logic for tracking update
state, further simplifying the codebase.
[[1]](diffhunk://#diff-aea3404667e7a3de2750bf9ab7ee8ff5e717892caa68ee1de86713cf8e21b44cL53)
[[2]](diffhunk://#diff-aea3404667e7a3de2750bf9ab7ee8ff5e717892caa68ee1de86713cf8e21b44cL328)
[[3]](diffhunk://#diff-aea3404667e7a3de2750bf9ab7ee8ff5e717892caa68ee1de86713cf8e21b44cL343-L346)

**Project and Namespace Adjustments:**

* Updated the project file to include all localized resource files
recursively, improving localization support.
* Added a missing namespace import to ensure proper type resolution.
* Added a missing `using System;` directive for the `Action` delegate.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-09 13:06:50 +08:00
Gordon Lam
a246789719 docs(prompts): sync commit/pr guidance and Copilot settings (#44627)
## Summary of the Pull Request
- Clarified the PR-summary prompt to prepend a PR title and to reuse the
Conventional Commit rules from
`.github/prompts/create-commit-title.prompt.md`.
- Expanded the commit-title prompt with clearer purpose, inputs, and
Conventional Commit guidance.
- Added workspace Copilot chat settings in `.vscode/settings.json` to
point review/commit/PR generation at the repo prompt files.

e.g. for commit tile generation:
<img width="562" height="376" alt="image"
src="https://github.com/user-attachments/assets/ca11d117-e4ad-4d1e-abb7-2b4600690f45"
/>


## PR Checklist
- [ ] Closes: N/A
- [ ] Communication: N/A (prompt/settings maintenance)
- [ ] Tests: Not run (prompt/settings-only change)

## Detailed Description of the Pull Request / Additional comments
- The PR-summary workflow now directs PR title generation to the
existing commit-title prompt instead of duplicating rules, and places
the title above the filled template.
- The commit-title prompt now spells out required diff command, decision
steps, and Conventional Commit examples.
- VS Code Copilot chat settings ensure review, commit, and PR
description generation use the repository prompts consistently.

## Validation Steps Performed
- Not run (no product code changes)
2026-01-09 11:38:05 +08:00
Shawn Yuan
af401dd6e9 Fix quickaccess localization issue (#44626)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request updates the resource file inclusion pattern in the
`PowerToys.QuickAccess.csproj` project file to support localization for
multiple languages.

Localization improvements:

* Changed the `PRIResource` include path to recursively include all
`Resources.resw` files from subdirectories, enabling support for
multiple language resource files instead of just `en-us`.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-09 10:27:30 +08:00
MemphiZ
6c2a99dfd6 Fix FancyZones editor overlay on mixed-DPI multi-monitor setups (#44440)
## Summary of the Pull Request

Use DPI-unaware context when positioning overlay windows to match the
coordinate space from the C++ backend.

The FancyZones editor overlay windows were incorrectly positioned on
secondary monitors when using different DPI scaling (e.g.,
125%/150%/125%). Zones appeared shifted or clipped because they extended
past monitor edges.

## PR Checklist

- [x] Closes: #43363
- [x] Closes: #43386
- [ ] **Communication:** I've discussed this with core contributors
already.
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places

## Detailed Description of the Pull Request / Additional comments

### Root Cause
The C++ backend uses a DPI-unaware thread to get virtual screen
coordinates, but the WPF editor (PerMonitorV2 DPI-aware) interpreted
these coordinates with DPI scaling applied, causing misalignment on
non-primary monitors.

### Fix
- **EditorParameters.cpp**: Use consistent virtual coordinates for all
monitor properties (removed `DPIAware::Convert` that was only applied to
dimensions)
- **Monitor.cs**: Reposition overlay windows using DPI-unaware context
after HWND creation, matching the coordinate space from C++ backend
- **NativeMethods.cs**: Added `SetWindowPositionDpiUnaware()` using
`SetThreadDpiAwarenessContext` to temporarily switch DPI awareness

## Validation Steps Performed

Manually tested on 3-monitor setup with 125%/150%/125% DPI scaling -
overlays now correctly cover each monitor's work area.

---

🤖 *This fix was developed with [Claude Code](https://claude.ai/code)
after 6 hours of debugging DPI coordinate systems together.*

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2026-01-09 09:57:58 +08:00
Mike Griese
7cf32bf204 cmdpal: bump to 0.8 (#44622)
title
2026-01-09 09:55:27 +08:00
Jiří Polášek
ae9ba62a40 CmdPal: Remove subtitle from all built-in top level commands (#44621)
## Summary of the Pull Request

This PR removes subtitles from all built-in top-level commands, except
for fallbacks, apps, and bookmarks.

<img width="885" height="987" alt="image"
src="https://github.com/user-attachments/assets/4da7377d-2c86-4658-a96b-33a881333639"
/>

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-09 09:42:06 +08:00
46 changed files with 587 additions and 208 deletions

View File

@@ -6,13 +6,45 @@ description: 'Generate an 80-character git commit title for the local diff'
# Generate Commit Title
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
## Purpose
Provide a single-line, ready-to-paste git commit title (<= 80 characters) that reflects 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`.
## Input to collect
- Run exactly one command to view the local diff:
```@terminal
git diff HEAD
```
## How to decide the title
1. From the diff, find the dominant area (e.g., `src/modules/*`, `doc/devdocs/**`) and the change type (bug fix, docs update, config tweak).
2. Draft an imperative, plain-ASCII title that:
- Mentions the primary component when obvious (e.g., `FancyZones:` or `Docs:`)
- Stays within 80 characters and has no trailing punctuation
## Final output
- Reply with only the commit title on a single line—no extra text.
## PR title convention (when asked)
Use Conventional Commits style:
`<type>(<scope>): <summary>`
**Allowed types**
- feat, fix, docs, refactor, perf, test, build, ci, chore
**Scope rules**
- Use a short, PowerToys-focused scope (one word preferred). Common scopes:
- Core: `runner`, `settings-ui`, `common`, `docs`, `build`, `ci`, `installer`, `gpo`, `dsc`
- Modules: `fancyzones`, `powerrename`, `awake`, `colorpicker`, `imageresizer`, `keyboardmanager`, `mouseutils`, `peek`, `hosts`, `file-locksmith`, `screen-ruler`, `text-extractor`, `cropandlock`, `paste`, `powerlauncher`
- If unclear, pick the closest module or subsystem; omit only if unavoidable
**Summary rules**
- Imperative, present tense (“add”, “update”, “remove”, “fix”)
- Keep it <= 72 characters when possible; be specific, avoid “misc changes”
**Examples**
- `feat(fancyzones): add canvas template duplication`
- `fix(mouseutils): guard crosshair toggle when dpi info missing`
- `docs(runner): document tray icon states`
- `build(installer): align wix v5 suffix flag`
- `ci(ci): cache pipeline artifacts for x64`

View File

@@ -22,3 +22,4 @@ description: 'Generate a PowerToys-ready pull request description from the local
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.
8. Prepend the PR title above the filled template, applying the Conventional Commit type/scope rules from `.github/prompts/create-commit-title.prompt.md`; pick the dominant component from the diff and keep the title concise and imperative.

View File

@@ -10,8 +10,8 @@ description: 'Resolve Code scanning / check-spelling comments on the active PR'
**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.
- Prefer improving the wording in the originally flagged file when it clarifies intent without changing meaning; if the wording is already clear/standard for the context, handle it via `.github/actions/spell-check/expect.txt` and reuse existing entries.
- Limit edits to the flagged text and `.github/actions/spell-check/expect.txt`; leave all other files and topics untouched.
**Prerequisites:**
- Install GitHub CLI if it is not present: `winget install GitHub.cli`.
@@ -20,5 +20,6 @@ description: 'Resolve Code scanning / check-spelling comments on the active PR'
**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.
3. For each flagged token, first consider tightening or rephrasing the original text to avoid the false positive while keeping the meaning intact; if the existing wording is already normal and professional for the context, proceed to allowlisting instead of changing it.
4. When allowlisting, 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.
5. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.

17
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"github.copilot.chat.reviewSelection.instructions": [
{
"file": ".github/prompts/review-pr.prompt.md"
}
],
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"file": ".github/prompts/create-commit-title.prompt.md"
}
],
"github.copilot.chat.pullRequestDescriptionGeneration.instructions": [
{
"file": ".github/prompts/create-pr-summary.prompt.md"
}
]
}

View File

@@ -66,5 +66,10 @@ namespace PowerToys.GPOWrapperProjection
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
}
public static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue()
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredLightSwitchEnabledValue();
}
}
}

View File

@@ -70,6 +70,8 @@ namespace CommonSharedConstants
// Path to the event used by AlwaysOnTop
const wchar_t ALWAYS_ON_TOP_PIN_EVENT[] = L"Local\\AlwaysOnTopPinEvent-892e0aa2-cfa8-4cc4-b196-ddeb32314ce8";
const wchar_t ALWAYS_ON_TOP_TRANSPARENT_PIN_EVENT[] = L"Local\\AlwaysOnTopTransparentPinEvent-a]bc123-4567-89ab-cdef01234567";
const wchar_t ALWAYS_ON_TOP_TERMINATE_EVENT[] = L"Local\\AlwaysOnTopTerminateEvent-cfdf1eae-791f-4953-8021-2f18f3837eae";
// Path to the event used by PowerAccent

View File

@@ -85,3 +85,72 @@ inline T GetWindowParam(HWND window)
{
return reinterpret_cast<T>(GetWindowLongPtrW(window, GWLP_USERDATA));
}
// Structure to store window transparency properties for restoration
struct WindowTransparencyProperties
{
long exstyle = 0;
COLORREF crKey = RGB(0, 0, 0);
DWORD dwFlags = 0;
BYTE alpha = 0;
bool transparencySet = false;
};
// Makes a window transparent by setting layered window attributes
// alphaPercent: transparency level from 0-100 (50 = 50% transparent)
// Returns the saved properties that can be used to restore the window later
inline WindowTransparencyProperties MakeWindowTransparent(HWND window, int alphaPercent = 50)
{
WindowTransparencyProperties props{};
if (!window || alphaPercent < 0 || alphaPercent > 100)
{
return props;
}
props.exstyle = GetWindowLong(window, GWL_EXSTYLE);
// Add WS_EX_LAYERED style to enable transparency
SetWindowLong(window, GWL_EXSTYLE, props.exstyle | WS_EX_LAYERED);
// Get current layered window attributes
if (!GetLayeredWindowAttributes(window, &props.crKey, &props.alpha, &props.dwFlags))
{
return props;
}
// Set new transparency level
BYTE alphaValue = static_cast<BYTE>((255 * alphaPercent) / 100);
if (!SetLayeredWindowAttributes(window, 0, alphaValue, LWA_ALPHA))
{
return props;
}
props.transparencySet = true;
return props;
}
// Restores window transparency to its original state
inline bool RestoreWindowTransparency(HWND window, const WindowTransparencyProperties& props)
{
if (!window || !props.transparencySet)
{
return false;
}
bool success = true;
// Restore original transparency attributes
if (!SetLayeredWindowAttributes(window, props.crKey, props.alpha, props.dwFlags))
{
success = false;
}
// Restore original extended style
if (SetWindowLong(window, GWL_EXSTYLE, props.exstyle) == 0)
{
success = false;
}
return success;
}

View File

@@ -32,7 +32,7 @@ bool isExcluded(HWND window)
}
AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps}),
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::TransparencyHotkey, SettingId::ExcludeApps}),
m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase)),
m_useCentralizedLLKH(useLLKH),
m_mainThreadId(mainThreadId),
@@ -105,6 +105,11 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
RegisterHotkey();
}
break;
case SettingId::TransparencyHotkey:
{
RegisterHotkey();
}
break;
case SettingId::FrameEnabled:
{
if (AlwaysOnTopSettings::settings().enableFrame)
@@ -153,9 +158,17 @@ LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lp
{
if (message == WM_HOTKEY)
{
int hotkeyId = static_cast<int>(wparam);
if (HWND fw{ GetForegroundWindow() })
{
ProcessCommand(fw);
if (hotkeyId == static_cast<int>(HotkeyId::Pin))
{
ProcessCommand(fw, false);
}
else if (hotkeyId == static_cast<int>(HotkeyId::TransparentPin))
{
ProcessCommand(fw, true);
}
}
}
else if (message == WM_PRIV_SETTINGS_CHANGED)
@@ -166,7 +179,7 @@ LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lp
return 0;
}
void AlwaysOnTop::ProcessCommand(HWND window)
void AlwaysOnTop::ProcessCommand(HWND window, bool transparent)
{
bool gameMode = detect_game_mode();
if (AlwaysOnTopSettings::settings().blockInGameMode && gameMode)
@@ -191,6 +204,16 @@ void AlwaysOnTop::ProcessCommand(HWND window)
m_topmostWindows.erase(iter);
}
// Restore transparency if the window has layered style
if (transparent)
{
LONG exStyle = GetWindowLong(window, GWL_EXSTYLE);
if (exStyle & WS_EX_LAYERED)
{
SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
}
}
Trace::AlwaysOnTop::UnpinWindow();
}
}
@@ -200,6 +223,12 @@ void AlwaysOnTop::ProcessCommand(HWND window)
{
soundType = Sound::Type::On;
AssignBorder(window);
if (transparent)
{
::MakeWindowTransparent(window, AlwaysOnTopSettings::settings().transparencyPercentage);
}
Trace::AlwaysOnTop::PinWindow();
}
}
@@ -274,6 +303,9 @@ void AlwaysOnTop::RegisterHotkey() const
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::Pin));
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code());
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::TransparentPin));
RegisterHotKey(m_window, static_cast<int>(HotkeyId::TransparentPin), AlwaysOnTopSettings::settings().transparencyHotkey.get_modifiers(), AlwaysOnTopSettings::settings().transparencyHotkey.get_code());
}
void AlwaysOnTop::RegisterLLKH()
@@ -284,6 +316,7 @@ void AlwaysOnTop::RegisterLLKH()
}
m_hPinEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
m_hTransparentPinEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_TRANSPARENT_PIN_EVENT);
m_hTerminateEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_TERMINATE_EVENT);
if (!m_hPinEvent)
@@ -292,20 +325,27 @@ void AlwaysOnTop::RegisterLLKH()
return;
}
if (!m_hTransparentPinEvent)
{
Logger::warn(L"Failed to create transparentPinEvent. {}", get_last_error_or_default(GetLastError()));
return;
}
if (!m_hTerminateEvent)
{
Logger::warn(L"Failed to create terminateEvent. {}", get_last_error_or_default(GetLastError()));
return;
}
HANDLE handles[2] = { m_hPinEvent,
HANDLE handles[3] = { m_hPinEvent,
m_hTransparentPinEvent,
m_hTerminateEvent };
m_thread = std::thread([this, handles]() {
MSG msg;
while (m_running)
{
DWORD dwEvt = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT);
DWORD dwEvt = MsgWaitForMultipleObjects(3, handles, false, INFINITE, QS_ALLINPUT);
if (!m_running)
{
break;
@@ -315,13 +355,19 @@ void AlwaysOnTop::RegisterLLKH()
case WAIT_OBJECT_0:
if (HWND fw{ GetForegroundWindow() })
{
ProcessCommand(fw);
ProcessCommand(fw, false);
}
break;
case WAIT_OBJECT_0 + 1:
PostThreadMessage(m_mainThreadId, WM_QUIT, 0, 0);
if (HWND fw{ GetForegroundWindow() })
{
ProcessCommand(fw, true);
}
break;
case WAIT_OBJECT_0 + 2:
PostThreadMessage(m_mainThreadId, WM_QUIT, 0, 0);
break;
case WAIT_OBJECT_0 + 3:
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);

View File

@@ -10,6 +10,7 @@
#include <common/hooks/WinHookEvent.h>
#include <common/notifications/NotificationUtil.h>
#include <common/utils/window.h>
class AlwaysOnTop : public SettingsObserver
{
@@ -38,6 +39,7 @@ private:
enum class HotkeyId : int
{
Pin = 1,
TransparentPin = 2,
};
static inline AlwaysOnTop* s_instance = nullptr;
@@ -49,6 +51,7 @@ private:
HINSTANCE m_hinstance;
std::map<HWND, std::unique_ptr<WindowBorder>> m_topmostWindows{};
HANDLE m_hPinEvent;
HANDLE m_hTransparentPinEvent;
HANDLE m_hTerminateEvent;
DWORD m_mainThreadId;
std::thread m_thread;
@@ -64,7 +67,7 @@ private:
void RegisterLLKH();
void SubscribeToEvents();
void ProcessCommand(HWND window);
void ProcessCommand(HWND window, bool transparent = false);
void StartTrackingTopmostWindows();
void UnpinAll();
void CleanUp();

View File

@@ -13,6 +13,7 @@ namespace NonLocalizable
const static wchar_t* SettingsFileName = L"settings.json";
const static wchar_t* HotkeyID = L"hotkey";
const static wchar_t* TransparencyHotkeyID = L"transparency-hotkey";
const static wchar_t* SoundEnabledID = L"sound-enabled";
const static wchar_t* FrameEnabledID = L"frame-enabled";
const static wchar_t* FrameThicknessID = L"frame-thickness";
@@ -22,6 +23,7 @@ namespace NonLocalizable
const static wchar_t* ExcludedAppsID = L"excluded-apps";
const static wchar_t* FrameAccentColor = L"frame-accent-color";
const static wchar_t* RoundCornersEnabledID = L"round-corners-enabled";
const static wchar_t* TransparencyPercentageID = L"transparency-percentage";
}
// TODO: move to common utils
@@ -104,6 +106,26 @@ void AlwaysOnTopSettings::LoadSettings()
NotifyObservers(SettingId::Hotkey);
}
}
if (const auto jsonVal = values.get_json(NonLocalizable::TransparencyHotkeyID))
{
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
if (m_settings.transparencyHotkey.get_modifiers() != val.get_modifiers() || m_settings.transparencyHotkey.get_key() != val.get_key() || m_settings.transparencyHotkey.get_code() != val.get_code())
{
m_settings.transparencyHotkey = val;
NotifyObservers(SettingId::TransparencyHotkey);
}
}
if (const auto jsonVal = values.get_int_value(NonLocalizable::TransparencyPercentageID))
{
auto val = *jsonVal;
if (m_settings.transparencyPercentage != val)
{
m_settings.transparencyPercentage = val;
NotifyObservers(SettingId::TransparencyPercentage);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::SoundEnabledID))
{

View File

@@ -15,6 +15,8 @@ class SettingsObserver;
struct Settings
{
PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T
PowerToysSettings::HotkeyObject transparencyHotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, true, 84); // win + ctrl + shift + T
int transparencyPercentage = 50; // 0-100, 0 = fully transparent, 100 = opaque
bool enableFrame = true;
bool enableSound = true;
bool roundCornersEnabled = true;

View File

@@ -11,5 +11,7 @@ enum class SettingId
BlockInGameMode,
ExcludeApps,
FrameAccentColor,
RoundCornersEnabled
RoundCornersEnabled,
TransparencyHotkey,
TransparencyPercentage
};

View File

@@ -27,6 +27,7 @@ namespace
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_HOTKEY[] = L"hotkey";
const wchar_t JSON_KEY_TRANSPARENCY_HOTKEY[] = L"transparency-hotkey";
const wchar_t JSON_KEY_VALUE[] = L"value";
}
@@ -105,17 +106,24 @@ public:
}
}
virtual bool on_hotkey(size_t /*hotkeyId*/) override
virtual bool on_hotkey(size_t hotkeyId) override
{
if (m_enabled)
{
Logger::trace(L"AlwaysOnTop hotkey pressed");
Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
if (!is_process_running())
{
Enable();
}
SetEvent(m_hPinEvent);
if (hotkeyId == 0)
{
SetEvent(m_hPinEvent);
}
else if (hotkeyId == 1)
{
SetEvent(m_hTransparentPinEvent);
}
return true;
}
@@ -125,19 +133,32 @@ public:
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
size_t count = 0;
if (m_hotkey.key)
{
if (hotkeys && buffer_size >= 1)
if (hotkeys && buffer_size > count)
{
hotkeys[0] = m_hotkey;
hotkeys[count] = m_hotkey;
Logger::trace(L"AlwaysOnTop hotkey[0]: win={}, ctrl={}, shift={}, alt={}, key={}",
m_hotkey.win, m_hotkey.ctrl, m_hotkey.shift, m_hotkey.alt, m_hotkey.key);
}
return 1;
count++;
}
else
if (m_transparencyHotkey.key)
{
return 0;
if (hotkeys && buffer_size > count)
{
hotkeys[count] = m_transparencyHotkey;
Logger::trace(L"AlwaysOnTop hotkey[1]: win={}, ctrl={}, shift={}, alt={}, key={}",
m_transparencyHotkey.win, m_transparencyHotkey.ctrl, m_transparencyHotkey.shift, m_transparencyHotkey.alt, m_transparencyHotkey.key);
}
count++;
}
Logger::trace(L"AlwaysOnTop get_hotkeys returning count={}", count);
return count;
}
// Enable the powertoy
@@ -174,6 +195,7 @@ public:
app_name = L"AlwaysOnTop"; //TODO: localize
app_key = NonLocalizable::ModuleKey;
m_hPinEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
m_hTransparentPinEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_TRANSPARENT_PIN_EVENT);
m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_TERMINATE_EVENT);
init_settings();
}
@@ -190,6 +212,7 @@ private:
std::wstring executable_args = L"";
executable_args.append(std::to_wstring(powertoys_pid));
ResetEvent(m_hPinEvent);
ResetEvent(m_hTransparentPinEvent);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
@@ -215,6 +238,7 @@ private:
{
m_enabled = false;
ResetEvent(m_hPinEvent);
ResetEvent(m_hTransparentPinEvent);
// Log telemetry
if (traceEvent)
@@ -253,6 +277,26 @@ private:
{
Logger::error("Failed to initialize AlwaysOnTop start shortcut");
}
try
{
auto jsonTransparencyHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_TRANSPARENCY_HOTKEY).GetNamedObject(JSON_KEY_VALUE);
m_transparencyHotkey.win = jsonTransparencyHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
m_transparencyHotkey.alt = jsonTransparencyHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
m_transparencyHotkey.shift = jsonTransparencyHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_transparencyHotkey.ctrl = jsonTransparencyHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_transparencyHotkey.key = static_cast<unsigned char>(jsonTransparencyHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
}
catch (...)
{
Logger::info("AlwaysOnTop transparency shortcut not configured, using default");
// Set default: Win + Ctrl + Shift + T
m_transparencyHotkey.win = true;
m_transparencyHotkey.ctrl = true;
m_transparencyHotkey.shift = true;
m_transparencyHotkey.alt = false;
m_transparencyHotkey.key = 0x54; // T
}
}
else
{
@@ -288,9 +332,11 @@ private:
bool m_enabled = false;
HANDLE m_hProcess = nullptr;
Hotkey m_hotkey;
Hotkey m_transparencyHotkey;
// Handle to event used to pin/unpin windows
HANDLE m_hPinEvent;
HANDLE m_hTransparentPinEvent;
HANDLE m_hTerminateEvent;
};

View File

@@ -21,7 +21,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() =>
[
new CommandItem(openSettings) { },
new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle },
new CommandItem(_newExtension) { Title = _newExtension.Title },
];
public override IFallbackCommandItem[] FallbackCommands() =>

View File

@@ -90,20 +90,5 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
// Assert
Assert.IsFalse(string.IsNullOrEmpty(displayName));
}
[TestMethod]
public void GetTranslatedPluginDescriptionTest()
{
// Setup
var provider = new TimeDateCommandsProvider();
// Act
var commands = provider.TopLevelCommands();
var subtitle = commands[0].Subtitle;
// Assert
Assert.IsFalse(string.IsNullOrEmpty(subtitle));
Assert.IsTrue(subtitle.Contains("Show time and date values in different formats"));
}
}
}

View File

@@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor>
<VersionMinor>7</VersionMinor>
<VersionMinor>8</VersionMinor>
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -15,7 +15,6 @@ public partial class CalculatorCommandProvider : CommandProvider
private static ISettingsInterface settings = new SettingsManager();
private readonly ListItem _listItem = new(new CalculatorListPage(settings))
{
Subtitle = Resources.calculator_top_level_subtitle,
MoreCommands = [new CommandContextItem(((SettingsManager)settings).Settings.SettingsPage)],
};

View File

@@ -19,7 +19,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
{
Title = Properties.Resources.list_item_title,
Subtitle = Properties.Resources.list_item_subtitle,
Icon = Icons.ClipboardListIcon,
MoreCommands = [
new CommandContextItem(_settingsManager.Settings.SettingsPage),

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.CommandPalette.Extensions;
@@ -41,15 +42,19 @@ internal sealed partial class FancyZonesMonitorListItem : ListItem
public static Details BuildMonitorDetails(FancyZonesMonitorDescriptor monitor)
{
var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString();
// Calculate physical resolution from logical pixels and DPI
var scaleFactor = monitor.Data.Dpi > 0 ? monitor.Data.Dpi / 96.0 : 1.0;
var physicalWidth = (int)Math.Round(monitor.Data.MonitorWidth * scaleFactor);
var physicalHeight = (int)Math.Round(monitor.Data.MonitorHeight * scaleFactor);
var resolution = $"{physicalWidth}\u00D7{physicalHeight}";
var tags = new List<IDetailsElement>
{
DetailTag(Resources.FancyZones_Monitor, monitor.Data.Monitor),
DetailTag(Resources.FancyZones_Instance, monitor.Data.MonitorInstanceId),
DetailTag(Resources.FancyZones_Serial, monitor.Data.MonitorSerialNumber),
DetailTag(Resources.FancyZones_Number, monitor.Data.MonitorNumber.ToString(CultureInfo.InvariantCulture)),
DetailTag(Resources.FancyZones_VirtualDesktop, currentVirtualDesktop),
DetailTag(Resources.FancyZones_WorkArea, $"{monitor.Data.LeftCoordinate},{monitor.Data.TopCoordinate} {monitor.Data.WorkAreaWidth}\u00D7{monitor.Data.WorkAreaHeight}"),
DetailTag(Resources.FancyZones_Resolution, $"{monitor.Data.MonitorWidth}\u00D7{monitor.Data.MonitorHeight}"),
DetailTag(Resources.FancyZones_Resolution, resolution),
DetailTag(Resources.FancyZones_DPI, monitor.Data.Dpi.ToString(CultureInfo.InvariantCulture)),
};

View File

@@ -19,8 +19,12 @@ internal readonly record struct FancyZonesMonitorDescriptor(
{
get
{
var size = $"{Data.MonitorWidth}×{Data.MonitorHeight}";
var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(Data.Dpi * 100 / 96.0)) : "n/a";
// MonitorWidth/Height are logical (DPI-scaled) pixels, calculate physical resolution
var scaleFactor = Data.Dpi > 0 ? Data.Dpi / 96.0 : 1.0;
var physicalWidth = (int)Math.Round(Data.MonitorWidth * scaleFactor);
var physicalHeight = (int)Math.Round(Data.MonitorHeight * scaleFactor);
var size = $"{physicalWidth}×{physicalHeight}";
var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(scaleFactor * 100)) : "n/a";
return $"{size} \u2022 {scaling}";
}
}

View File

@@ -485,12 +485,21 @@ internal static class FancyZonesThumbnailRenderer
private static List<NormalizedRect> GetFocusRects(int zoneCount)
{
// Focus layout parameters from FancyZonesEditor CanvasLayoutModel:
// - DefaultOffset = 100px from top-left (normalized: ~0.05 for typical screen)
// - OffsetShift = 50px per zone (normalized: ~0.025)
// - ZoneSizeMultiplier = 0.4 (zones are 40% of screen)
zoneCount = Math.Clamp(zoneCount, 1, 8);
var rects = new List<NormalizedRect>(zoneCount);
const float defaultOffset = 0.05f; // ~100px on 1920px screen
const float offsetShift = 0.025f; // ~50px on 1920px screen
const float zoneSize = 0.4f; // 40% of screen
for (var i = 0; i < zoneCount; i++)
{
var offset = i * 0.06f;
rects.Add(new NormalizedRect(0.1f + offset, 0.1f + offset, 0.8f, 0.8f));
var offset = i * offsetShift;
rects.Add(new NormalizedRect(defaultOffset + offset, defaultOffset + offset, zoneSize, zoneSize));
}
return rects;

View File

@@ -31,7 +31,6 @@ public partial class RemoteDesktopCommandProvider : CommandProvider
listPageCommand = new CommandItem(listPage)
{
Subtitle = Resources.remotedesktop_subtitle,
Icon = Icons.RDPIcon,
MoreCommands = [
new CommandContextItem(settingsManager.Settings.SettingsPage),

View File

@@ -39,7 +39,6 @@ public partial class ShellCommandsProvider : CommandProvider
{
Icon = Icons.RunV2Icon,
Title = Resources.shell_command_name,
Subtitle = Resources.cmd_plugin_description,
MoreCommands = [
new CommandContextItem(Settings.SettingsPage),
],

View File

@@ -28,7 +28,6 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
{
Icon = _timeDateExtensionPage.Icon,
Title = Resources.Microsoft_plugin_timedate_plugin_name,
Subtitle = GetTranslatedPluginDescription(),
MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
};

View File

@@ -26,7 +26,6 @@ public partial class WindowWalkerCommandsProvider : CommandProvider
_windowWalkerPageItem = new CommandItem(new WindowWalkerListPage())
{
Title = Resources.window_walker_top_level_command_title,
Subtitle = Resources.windowwalker_name,
MoreCommands = [
new CommandContextItem(Settings.SettingsPage),
],

View File

@@ -30,7 +30,6 @@ public sealed partial class WindowsSettingsCommandsProvider : CommandProvider
_searchSettingsListItem = new CommandItem(new WindowsSettingsListPage(_windowsSettings))
{
Title = Resources.settings_title,
Subtitle = Resources.settings_subtitle,
};
_fallback = new(_windowsSettings);

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using static FancyZonesEditorCommon.Data.CustomLayouts;
@@ -23,8 +24,10 @@ namespace FancyZonesEditorCommon.Data
{
public struct CanvasZoneWrapper
{
[JsonPropertyName("X")]
public int X { get; set; }
[JsonPropertyName("Y")]
public int Y { get; set; }
public int Width { get; set; }

View File

@@ -191,27 +191,22 @@ bool EditorParameters::Save(const WorkAreaConfiguration& configuration, OnThread
monitorJson.dpi = dpi;
MONITORINFOEX monitorInfo{};
// Get DPI-unaware values for dimensions (virtual coordinates for WPF sizing)
MONITORINFOEX monitorInfoUnaware{};
dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] {
monitorInfo.cbSize = sizeof(monitorInfo);
if (!GetMonitorInfo(monitor, &monitorInfo))
{
return;
}
monitorInfoUnaware.cbSize = sizeof(monitorInfoUnaware);
GetMonitorInfo(monitor, &monitorInfoUnaware);
} }).wait();
float width = static_cast<float>(monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left);
float height = static_cast<float>(monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top);
DPIAware::Convert(monitor, width, height);
// Dimensions in virtual coordinates (from DPI-unaware thread)
monitorJson.monitorWidth = monitorInfoUnaware.rcMonitor.right - monitorInfoUnaware.rcMonitor.left;
monitorJson.monitorHeight = monitorInfoUnaware.rcMonitor.bottom - monitorInfoUnaware.rcMonitor.top;
monitorJson.workAreaWidth = monitorInfoUnaware.rcWork.right - monitorInfoUnaware.rcWork.left;
monitorJson.workAreaHeight = monitorInfoUnaware.rcWork.bottom - monitorInfoUnaware.rcWork.top;
monitorJson.monitorWidth = static_cast<int>(std::roundf(width));
monitorJson.monitorHeight = static_cast<int>(std::roundf(height));
// use dpi-unaware values
monitorJson.top = monitorInfo.rcWork.top;
monitorJson.left = monitorInfo.rcWork.left;
monitorJson.workAreaWidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left;
monitorJson.workAreaHeight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top;
// Position in virtual coordinates (matched by DPI-unaware context in WPF editor)
monitorJson.left = monitorInfoUnaware.rcWork.left;
monitorJson.top = monitorInfoUnaware.rcWork.top;
argsJson.monitors.emplace_back(std::move(monitorJson));
}

View File

@@ -12,6 +12,7 @@
#include <FancyZonesLib/trace.h>
#include <common/utils/elevation.h>
#include <common/utils/window.h>
WindowDrag::WindowDrag(HWND window, const std::unordered_map<HMONITOR, std::unique_ptr<WorkArea>>& activeWorkAreas) :
m_window(window),
@@ -199,43 +200,15 @@ void WindowDrag::SetWindowTransparency()
{
if (FancyZonesSettings::settings().makeDraggedWindowTransparent)
{
m_windowProperties.exstyle = GetWindowLong(m_window, GWL_EXSTYLE);
SetWindowLong(m_window, GWL_EXSTYLE, m_windowProperties.exstyle | WS_EX_LAYERED);
if (!GetLayeredWindowAttributes(m_window, &m_windowProperties.crKey, &m_windowProperties.alpha, &m_windowProperties.dwFlags))
{
Logger::error(L"Window transparency: GetLayeredWindowAttributes failed, {}", get_last_error_or_default(GetLastError()));
return;
}
if (!SetLayeredWindowAttributes(m_window, 0, (255 * 50) / 100, LWA_ALPHA))
{
Logger::error(L"Window transparency: SetLayeredWindowAttributes failed, {}", get_last_error_or_default(GetLastError()));
return;
}
m_windowProperties.transparencySet = true;
m_windowProperties.transparency = ::MakeWindowTransparent(m_window, 50);
}
}
void WindowDrag::ResetWindowTransparency()
{
if (FancyZonesSettings::settings().makeDraggedWindowTransparent && m_windowProperties.transparencySet)
if (FancyZonesSettings::settings().makeDraggedWindowTransparent && m_windowProperties.transparency.transparencySet)
{
bool reset = true;
if (!SetLayeredWindowAttributes(m_window, m_windowProperties.crKey, m_windowProperties.alpha, m_windowProperties.dwFlags))
{
Logger::error(L"Window transparency: SetLayeredWindowAttributes failed, {}", get_last_error_or_default(GetLastError()));
reset = false;
}
if (SetWindowLong(m_window, GWL_EXSTYLE, m_windowProperties.exstyle) == 0)
{
Logger::error(L"Window transparency: SetWindowLong failed, {}", get_last_error_or_default(GetLastError()));
reset = false;
}
m_windowProperties.transparencySet = !reset;
::RestoreWindowTransparency(m_window, m_windowProperties.transparency);
m_windowProperties.transparency.transparencySet = false;
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include <FancyZonesLib/HighlightedZones.h>
#include <common/utils/window.h>
class WorkArea;
class WindowDrag
{
WindowDrag(HWND window, const std::unordered_map<HMONITOR, std::unique_ptr<WorkArea>>& activeWorkAreas);
public:
static std::unique_ptr<WindowDrag> Create(HWND window, const std::unordered_map<HMONITOR, std::unique_ptr<WorkArea>>& activeWorkAreas);
~WindowDrag();
bool MoveSizeStart(HMONITOR monitor, bool isSnapping);
void MoveSizeUpdate(HMONITOR monitor, POINT const& ptScreen, bool isSnapping, bool isSelectManyZonesState);
void MoveSizeEnd();
private:
void SwitchSnappingMode(bool isSnapping);
void SetWindowTransparency();
void ResetWindowTransparency();
struct WindowProperties
{
// True if the window is a top-level window that does not have a visible owner
bool hasNoVisibleOwner = false;
// True if the window is a standard window
bool isStandardWindow = false;
// Transparency properties for restoration
WindowTransparencyProperties transparency;
};
const HWND m_window;
WindowProperties m_windowProperties; // MoveSizeWindowInfo of the window at the moment when dragging started
const std::unordered_map<HMONITOR, std::unique_ptr<WorkArea>>& m_activeWorkAreas; // all WorkAreas on current virtual desktop, mapped with monitors
WorkArea* m_currentWorkArea; // "Active" WorkArea, where the move/size is happening. Will update as drag moves between monitors.
bool m_snappingMode{ false };
HighlightedZones m_highlightedZones;
};

View File

@@ -200,43 +200,15 @@ void WindowMouseSnap::SetWindowTransparency()
{
if (FancyZonesSettings::settings().makeDraggedWindowTransparent)
{
m_windowProperties.exstyle = GetWindowLong(m_window, GWL_EXSTYLE);
SetWindowLong(m_window, GWL_EXSTYLE, m_windowProperties.exstyle | WS_EX_LAYERED);
if (!GetLayeredWindowAttributes(m_window, &m_windowProperties.crKey, &m_windowProperties.alpha, &m_windowProperties.dwFlags))
{
Logger::error(L"Window transparency: GetLayeredWindowAttributes failed, {}", get_last_error_or_default(GetLastError()));
return;
}
if (!SetLayeredWindowAttributes(m_window, 0, (255 * 50) / 100, LWA_ALPHA))
{
Logger::error(L"Window transparency: SetLayeredWindowAttributes failed, {}", get_last_error_or_default(GetLastError()));
return;
}
m_windowProperties.transparencySet = true;
m_windowProperties.transparency = ::MakeWindowTransparent(m_window, 50);
}
}
void WindowMouseSnap::ResetWindowTransparency()
{
if (FancyZonesSettings::settings().makeDraggedWindowTransparent && m_windowProperties.transparencySet)
if (FancyZonesSettings::settings().makeDraggedWindowTransparent && m_windowProperties.transparency.transparencySet)
{
bool reset = true;
if (!SetLayeredWindowAttributes(m_window, m_windowProperties.crKey, m_windowProperties.alpha, m_windowProperties.dwFlags))
{
Logger::error(L"Window transparency: SetLayeredWindowAttributes failed, {}", get_last_error_or_default(GetLastError()));
reset = false;
}
if (SetWindowLong(m_window, GWL_EXSTYLE, m_windowProperties.exstyle) == 0)
{
Logger::error(L"Window transparency: SetWindowLong failed, {}", get_last_error_or_default(GetLastError()));
reset = false;
}
m_windowProperties.transparencySet = !reset;
::RestoreWindowTransparency(m_window, m_windowProperties.transparency);
m_windowProperties.transparency.transparencySet = false;
}
}

View File

@@ -2,6 +2,7 @@
#include <FancyZonesLib/HighlightedZones.h>
#include <common/notifications/NotificationUtil.h>
#include <common/utils/window.h>
class WorkArea;
@@ -27,12 +28,8 @@ private:
{
// True if the window is a top-level window that does not have a visible owner
bool hasNoVisibleOwner = false;
// Properties to restore after dragging
long exstyle = 0;
COLORREF crKey = RGB(0, 0, 0);
DWORD dwFlags = 0;
BYTE alpha = 0;
bool transparencySet{false};
// Transparency properties for restoration
WindowTransparencyProperties transparency;
};
const HWND m_window;

View File

@@ -67,10 +67,18 @@ namespace FancyZonesEditor.Models
Window.KeyUp += ((App)Application.Current).App_KeyUp;
Window.KeyDown += ((App)Application.Current).App_KeyDown;
// Store for DPI-unaware positioning
_virtualWorkArea = workArea;
// Set initial WPF properties
Window.Left = workArea.X;
Window.Top = workArea.Y;
Window.Width = workArea.Width;
Window.Height = workArea.Height;
// After HWND is created, reposition using DPI-unaware context
// This matches the C++ backend which uses a DPI-unaware thread
Window.SourceInitialized += OnWindowSourceInitialized;
}
public Monitor(string monitorName, string monitorInstanceId, string monitorSerialNumber, string virtualDesktop, int dpi, Rect workArea, Size monitorSize)
@@ -80,16 +88,33 @@ namespace FancyZonesEditor.Models
}
private LayoutSettings _settings;
private Rect _virtualWorkArea;
private void OnWindowSourceInitialized(object sender, EventArgs e)
{
// Reposition window using DPI-unaware context to match the virtual coordinates
// from the FancyZones C++ backend (which uses a DPI-unaware thread)
Utils.NativeMethods.SetWindowPositionDpiUnaware(
Window,
(int)_virtualWorkArea.X,
(int)_virtualWorkArea.Y,
(int)_virtualWorkArea.Width,
(int)_virtualWorkArea.Height);
}
public void Scale(double scaleFactor)
{
Device.Scale(scaleFactor);
var workArea = Device.WorkAreaRect;
Window.Left = workArea.X;
Window.Top = workArea.Y;
Window.Width = workArea.Width;
Window.Height = workArea.Height;
_virtualWorkArea = Device.WorkAreaRect;
// Use DPI-unaware positioning
Utils.NativeMethods.SetWindowPositionDpiUnaware(
Window,
(int)_virtualWorkArea.X,
(int)_virtualWorkArea.Y,
(int)_virtualWorkArea.Width,
(int)_virtualWorkArea.Height);
}
public void SetLayoutSettings(LayoutModel model)

View File

@@ -69,7 +69,11 @@ namespace FancyZonesEditor.Utils
}
else
{
return ScreenBoundsWidth + " × " + ScreenBoundsHeight;
// Convert virtual coordinates to physical resolution by applying DPI scale
double scale = DPI / 96.0;
int physicalWidth = (int)Math.Round(ScreenBoundsWidth * scale);
int physicalHeight = (int)Math.Round(ScreenBoundsHeight * scale);
return physicalWidth + " × " + physicalHeight;
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -17,14 +17,48 @@ namespace FancyZonesEditor.Utils
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
private const int GWL_EX_STYLE = -20;
private const int WS_EX_APPWINDOW = 0x00040000;
private const int WS_EX_TOOLWINDOW = 0x00000080;
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_NOACTIVATE = 0x0010;
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
public static void SetWindowStyleToolWindow(Window hwnd)
{
var helper = new WindowInteropHelper(hwnd).Handle;
_ = SetWindowLong(helper, GWL_EX_STYLE, (GetWindowLong(helper, GWL_EX_STYLE) | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW);
}
/// <summary>
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates
/// from the FancyZones C++ backend (which uses a DPI-unaware thread).
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
/// </summary>
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
{
var helper = new WindowInteropHelper(window).Handle;
if (helper != IntPtr.Zero)
{
// Temporarily switch to DPI-unaware context to position window.
// This matches how the C++ backend gets coordinates via dpiUnawareThread.
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
try
{
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
}
finally
{
SetThreadDpiAwarenessContext(oldContext);
}
}
}
}
}

View File

@@ -112,6 +112,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
eventHandle.Set();
}
return true;
case ModuleType.LightSwitch:
using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.LightSwitchToggleEvent()))
{
eventHandle.Set();
}
return true;
default:
return false;

View File

@@ -64,6 +64,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
AddFlyoutMenuItem(ModuleType.EnvironmentVariables);
AddFlyoutMenuItem(ModuleType.FancyZones);
AddFlyoutMenuItem(ModuleType.Hosts);
AddFlyoutMenuItem(ModuleType.LightSwitch);
AddFlyoutMenuItem(ModuleType.PowerLauncher);
AddFlyoutMenuItem(ModuleType.PowerOCR);
AddFlyoutMenuItem(ModuleType.RegistryPreview);
@@ -120,6 +121,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
ModuleType.ColorPicker => SettingsRepository<ColorPickerSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.FancyZones => SettingsRepository<FancyZonesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(),
ModuleType.LightSwitch => SettingsRepository<LightSwitchSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ToggleThemeHotkey.Value.ToString(),
ModuleType.PowerLauncher => SettingsRepository<PowerLauncherSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.ToString(),
ModuleType.PowerOCR => SettingsRepository<PowerOcrSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.Workspaces => SettingsRepository<WorkspacesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.Hotkey.Value.ToString(),

View File

@@ -11,6 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class AlwaysOnTopProperties
{
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, true, false, false, 0x54);
public static readonly HotkeySettings DefaultTransparencyHotkeyValue = new HotkeySettings(true, true, false, true, 0x54);
public const bool DefaultFrameEnabled = true;
public const int DefaultFrameThickness = 15;
public const string DefaultFrameColor = "#0099cc";
@@ -19,10 +20,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public const bool DefaultSoundEnabled = true;
public const bool DefaultDoNotActivateOnGameMode = true;
public const bool DefaultRoundCornersEnabled = true;
public const int DefaultTransparencyPercentage = 50;
public AlwaysOnTopProperties()
{
Hotkey = new KeyboardKeysProperty(DefaultHotkeyValue);
TransparencyHotkey = new KeyboardKeysProperty(DefaultTransparencyHotkeyValue);
FrameEnabled = new BoolProperty(DefaultFrameEnabled);
FrameThickness = new IntProperty(DefaultFrameThickness);
FrameColor = new StringProperty(DefaultFrameColor);
@@ -31,12 +34,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
SoundEnabled = new BoolProperty(DefaultSoundEnabled);
DoNotActivateOnGameMode = new BoolProperty(DefaultDoNotActivateOnGameMode);
RoundCornersEnabled = new BoolProperty(DefaultRoundCornersEnabled);
TransparencyPercentage = new IntProperty(DefaultTransparencyPercentage);
ExcludedApps = new StringProperty();
}
[JsonPropertyName("hotkey")]
public KeyboardKeysProperty Hotkey { get; set; }
[JsonPropertyName("transparency-hotkey")]
public KeyboardKeysProperty TransparencyHotkey { get; set; }
[JsonPropertyName("frame-enabled")]
public BoolProperty FrameEnabled { get; set; }
@@ -64,6 +71,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("round-corners-enabled")]
public BoolProperty RoundCornersEnabled { get; set; }
[JsonPropertyName("transparency-percentage")]
public IntProperty TransparencyPercentage { get; set; }
public string ToJsonString()
{
return JsonSerializer.Serialize(this);

View File

@@ -11,7 +11,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Settings.UI.Library
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class LightSwitchSettings : BasePTModuleSettings, ISettingsConfig, ICloneable, IHotkeyConfig
{

View File

@@ -56,7 +56,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonSerializable(typeof(HostsSettings))]
[JsonSerializable(typeof(ImageResizerSettings))]
[JsonSerializable(typeof(KeyboardManagerSettings))]
[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))]
[JsonSerializable(typeof(LightSwitchSettings))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
[JsonSerializable(typeof(MouseJumpSettings))]

View File

@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))]
[JsonSerializable(typeof(LightSwitchSettings))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
[JsonSerializable(typeof(MouseJumpSettings))]

View File

@@ -38,6 +38,22 @@
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<tkcontrols:SettingsCard
Name="AlwaysOnTopTransparencyActivationShortcut"
x:Uid="AlwaysOnTop_TransparencyActivationShortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.TransparencyHotkey, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="AlwaysOnTopTransparencyPercentage"
x:Uid="AlwaysOnTop_TransparencyPercentage"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="100"
Minimum="10"
Value="{x:Bind ViewModel.TransparencyPercentage, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="AlwaysOnTop_Behavior_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">

View File

@@ -50,7 +50,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var darkSettings = this.moduleSettingsRepository.SettingsConfig;
// Pass them into the ViewModel
this.ViewModel = new LightSwitchViewModel(darkSettings, this.sendConfigMsg);
this.ViewModel = new LightSwitchViewModel(this.generalSettingsRepository, darkSettings, ShellPage.SendDefaultIPCMessage);
this.ViewModel.PropertyChanged += ViewModel_PropertyChanged;
this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository);
@@ -185,7 +185,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// need to save the values
this.ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture);
this.ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture);
this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}<EFBFBD>, {this.ViewModel.Longitude}<EFBFBD>";
this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}°, {this.ViewModel.Longitude}°";
var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
@@ -293,18 +293,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void UpdateEnabledState(bool recommendedState)
{
var enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredLightSwitchEnabledValue();
if (enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
this.ViewModel.IsEnabledGpoConfigured = true;
this.ViewModel.EnabledGPOConfiguration = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
this.ViewModel.IsEnabled = recommendedState;
}
ViewModel.RefreshEnabledState();
}
private async void SyncLocationButton_Click(object sender, RoutedEventArgs e)

View File

@@ -3224,6 +3224,18 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="AlwaysOnTop_ActivationShortcut.Description" xml:space="preserve">
<value>Customize the shortcut to pin or unpin an app window</value>
</data>
<data name="AlwaysOnTop_TransparencyActivationShortcut.Header" xml:space="preserve">
<value>Transparent pin shortcut</value>
</data>
<data name="AlwaysOnTop_TransparencyActivationShortcut.Description" xml:space="preserve">
<value>Customize the shortcut to pin an app window with transparency</value>
</data>
<data name="AlwaysOnTop_TransparencyPercentage.Header" xml:space="preserve">
<value>Window opacity</value>
</data>
<data name="AlwaysOnTop_TransparencyPercentage.Description" xml:space="preserve">
<value>Set the opacity level for transparent pinned windows (10-100%)</value>
</data>
<data name="Oobe_AlwaysOnTop.Title" xml:space="preserve">
<value>Always On Top</value>
<comment>{Locked}</comment>

View File

@@ -49,6 +49,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings = moduleSettingsRepository.SettingsConfig;
_hotkey = Settings.Properties.Hotkey.Value;
_transparencyHotkey = Settings.Properties.TransparencyHotkey.Value;
_frameEnabled = Settings.Properties.FrameEnabled.Value;
_frameThickness = Settings.Properties.FrameThickness.Value;
_frameColor = Settings.Properties.FrameColor.Value;
@@ -57,6 +58,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_soundEnabled = Settings.Properties.SoundEnabled.Value;
_doNotActivateOnGameMode = Settings.Properties.DoNotActivateOnGameMode.Value;
_roundCornersEnabled = Settings.Properties.RoundCornersEnabled.Value;
_transparencyPercentage = Settings.Properties.TransparencyPercentage.Value;
_excludedApps = Settings.Properties.ExcludedApps.Value;
_windows11 = OSVersionHelper.IsWindows11();
@@ -83,7 +85,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [Hotkey],
[ModuleName] = [Hotkey, TransparencyHotkey],
};
return hotkeysDict;
@@ -144,6 +146,30 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public HotkeySettings TransparencyHotkey
{
get => _transparencyHotkey;
set
{
if (value != _transparencyHotkey)
{
_transparencyHotkey = value ?? AlwaysOnTopProperties.DefaultTransparencyHotkeyValue;
Settings.Properties.TransparencyHotkey.Value = _transparencyHotkey;
NotifyPropertyChanged();
// Using InvariantCulture as this is an IPC message
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
AlwaysOnTopSettings.ModuleName,
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.AlwaysOnTopSettings)));
}
}
}
public bool FrameEnabled
{
get => _frameEnabled;
@@ -204,6 +230,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public int TransparencyPercentage
{
get => _transparencyPercentage;
set
{
if (value != _transparencyPercentage)
{
_transparencyPercentage = value;
Settings.Properties.TransparencyPercentage.Value = value;
NotifyPropertyChanged();
}
}
}
public bool SoundEnabled
{
get => _soundEnabled;
@@ -305,6 +346,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _enabledStateIsGPOConfigured;
private bool _isEnabled;
private HotkeySettings _hotkey;
private HotkeySettings _transparencyHotkey;
private bool _frameEnabled;
private int _frameThickness;
private string _frameColor;
@@ -313,6 +355,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _soundEnabled;
private bool _doNotActivateOnGameMode;
private bool _roundCornersEnabled;
private int _transparencyPercentage;
private string _excludedApps;
private bool _windows11;
}

View File

@@ -14,8 +14,10 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Newtonsoft.Json.Linq;
using PowerToys.GPOWrapper;
using Settings.UI.Library;
using Settings.UI.Library.Helpers;
@@ -27,10 +29,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private Func<string, int> SendConfigMSG { get; }
private GeneralSettings GeneralSettingsConfig { get; set; }
public ObservableCollection<SearchLocation> SearchLocations { get; } = new();
public LightSwitchViewModel(LightSwitchSettings initialSettings = null, Func<string, int> ipcMSGCallBackFunc = null)
public LightSwitchViewModel(ISettingsRepository<GeneralSettings> settingsRepository, LightSwitchSettings initialSettings = null, Func<string, int> ipcMSGCallBackFunc = null)
{
ArgumentNullException.ThrowIfNull(settingsRepository);
GeneralSettingsConfig = settingsRepository.SettingsConfig;
InitializeEnabledValue();
_moduleSettings = initialSettings ?? new LightSwitchSettings();
SendConfigMSG = ipcMSGCallBackFunc;
@@ -58,6 +66,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return hotkeysDict;
}
private void InitializeEnabledValue()
{
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredLightSwitchEnabledValue();
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
_enabledStateIsGPOConfigured = true;
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isEnabled = GeneralSettingsConfig.Enabled.LightSwitch;
}
}
private void ForceLightNow()
{
Logger.LogInfo("Sending custom action: forceLight");
@@ -93,33 +116,26 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsEnabled
{
get
{
if (_enabledStateIsGPOConfigured)
{
return _enabledGPOConfiguration;
}
else
{
return _isEnabled;
}
}
get => _isEnabled;
set
{
if (_isEnabled != value)
if (_enabledStateIsGPOConfigured)
{
if (_enabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
}
// If it's GPO configured, shouldn't be able to change this state.
return;
}
if (value != _isEnabled)
{
_isEnabled = value;
RefreshEnabledState();
// Set the status in the general settings configuration
GeneralSettingsConfig.Enabled.LightSwitch = value;
OutGoingGeneralSettings snd = new OutGoingGeneralSettings(GeneralSettingsConfig);
NotifyPropertyChanged();
SendConfigMSG(snd.ToString());
OnPropertyChanged(nameof(IsEnabled));
}
}
}
@@ -127,24 +143,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
set
{
if (_enabledStateIsGPOConfigured != value)
{
_enabledStateIsGPOConfigured = value;
NotifyPropertyChanged();
}
}
}
public bool EnabledGPOConfiguration
public GpoRuleConfigured EnabledGPOConfiguration
{
get => _enabledGPOConfiguration;
get => _enabledGpoRuleConfiguration;
set
{
if (_enabledGPOConfiguration != value)
if (_enabledGpoRuleConfiguration != value)
{
_enabledGPOConfiguration = value;
_enabledGpoRuleConfiguration = value;
NotifyPropertyChanged();
}
}
@@ -575,7 +583,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
private bool _enabledStateIsGPOConfigured;
private bool _enabledGPOConfiguration;
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private LightSwitchSettings _moduleSettings;
private bool _isEnabled;
private HotkeySettings _toggleThemeHotkey;