Compare commits

...

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c412e85cfc Switch AI model from gpt-4o-mini to gpt-4o for better classification accuracy
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/0789f3f1-1c5e-4f3b-9d03-dd771793a01c

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-04-18 11:24:22 +00:00
copilot-swe-agent[bot]
049cbccd21 Extract magic numbers to named constants per review feedback
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/5188f8c6-4eea-4d6a-a1f5-44a10c0100d7

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-04-18 11:21:47 +00:00
copilot-swe-agent[bot]
4308eb65a9 Add scheduled workflow for auto-labeling issues missing Product-* labels
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/5188f8c6-4eea-4d6a-a1f5-44a10c0100d7

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-04-18 11:20:55 +00:00
moooyo
5520ae4cfa [PowerDisplay] Fix startup restore, volume init, and identify window lifecycle (#47051)
<!-- 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
- **Volume initialization**: Read VCP 0x62 on monitor discovery so
`CurrentVolume` reflects actual hardware state instead of staying at the
50% default.
- **Brightness capability check**: Guard brightness init behind
`SupportsBrightness` flag, consistent with contrast/volume handling.
- **IdentifyWindow lifecycle**: Replace fire-and-forget `Task.Delay`
with `DispatcherQueueTimer` (UI-thread-safe, stoppable on dispose). Swap
`Activate`/`PositionOnDisplay` order to eliminate first-show flicker.
- **Startup restore fix**: Change `MonitorStateEntry` fields to `int?`
so unset values (`null`) aren't confused with zero — prevents writing
default 0% brightness/volume to hardware on startup.
- **Restore/profile apply refactor**: Push value validation down to
`Set*Async` (continuous → `Math.Clamp`, discrete → capabilities check),
extract unified `TryRestore` helper, remove redundant `IsValueInRange`
and `> 0` checks.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] 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

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 08:41:14 +00:00
moooyo
beddc3b065 [ImageResizer] Fix JsonPropertyName forwarding in ObservableProperty generator (#47056)
<!-- 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 issue introduced by our recent WinUI 3 migration

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

- [x] Closes: #47055
<!-- - [ ] 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

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
2026-04-17 15:20:45 +08:00
Niels Laute
088da21a70 Update default module states (#47027)
## Summary

- **Disable 7 modules by default** for new users: PowerToys Run, Crop
and Lock, Advanced Paste, Hosts File Editor, Registry Preview,
Environment Variables, Workspaces
- **Swap default hotkeys**: Command Palette now defaults to \Alt+Space\,
PowerToys Run now defaults to \Win+Alt+Space\
- Update unit test to reflect PowerLauncher default-off state

## Changes

| File | Change |
|------|--------|
| \EnabledModules.cs\ | Set 7 module defaults to off |
| \PowerLauncherProperties.cs\ | Default hotkey → \Win+Alt+Space\ |
| \SettingsModel.cs\ (CmdPal) | Default hotkey → \Alt+Space\ |
| \General.cs\ (test) | Assert PowerLauncher is false |

## Validation

- Existing unit test updated to match new defaults
- No ABI or IPC contract changes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-16 08:44:25 +02:00
Gordon Lam
befb5c672e Fix AdvancedPaste auto-copy failing on Electron/Chromium apps (#46486)
## Summary

Fixes #46485

AdvancedPaste's auto-copy feature fails on Electron/Chromium-based apps
(e.g. Microsoft Teams, VS Code, browsers) because `WM_COPY` is delivered
successfully but silently ignored by these apps.

## Problem

The auto-copy code sends `WM_COPY` via `SendMessageTimeout`. For
standard Win32 controls this works, but Electron apps accept the message
delivery without actually copying to clipboard. The code treated
successful delivery as success and **never fell back to `SendInput`
Ctrl+C**.

## Changes


**`src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp`**:

- **Changed retry logic**: Each attempt now tries both `WM_COPY` and
`SendInput` Ctrl+C. If `WM_COPY` is delivered but clipboard is
unchanged, it falls through to Ctrl+C instead of giving up.
- **Extracted `poll_clipboard_sequence()` helper**: Reusable clipboard
polling logic (checks `GetClipboardSequenceNumber` over N polls with
configurable delay).
- **Extracted `send_ctrl_c_input()` helper**: Sends Ctrl+C via
`SendInput` with `CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG`.
- **Improved logging**: Each strategy logs clearly whether it succeeded
or fell through, making future debugging easier.

## Validation

- [x] Manual testing with Microsoft Teams (Electron): auto-copy now
works for selected text
- [x] Standard Win32 apps (Notepad, etc.): `WM_COPY` still works on
first try, no regression
- [x] No new warnings or errors in build

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-16 10:49:40 +08:00
15 changed files with 508 additions and 185 deletions

View File

@@ -0,0 +1,232 @@
name: Scheduled Issue Product Labeling
on:
schedule:
- cron: "20 */6 * * *" # Every 6 hours at :20
workflow_dispatch: # Allow manual trigger
permissions:
models: read
issues: write
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
label-issues:
runs-on: ubuntu-latest
steps:
- name: Label issues missing Product labels
uses: actions/github-script@v7
with:
script: |
// ── Product label mapping ──────────────────────────────────
// Canonical list of Product-* labels used in this repo,
// derived from .github/skills/release-note-generation/references/step2-labeling.md
const PRODUCT_LABELS = [
"Product-Advanced Paste",
"Product-Always on Top",
"Product-Awake",
"Product-ColorPicker",
"Product-Command not found",
"Product-Command Palette",
"Product-CropAndLock",
"Product-Cursor Wrap",
"Product-Environment Variables",
"Product-FancyZones",
"Product-File Explorer",
"Product-File Locksmith",
"Product-Find My Mouse",
"Product-Hosts",
"Product-Image Resizer",
"Product-Keyboard Manager",
"Product-LightSwitch",
"Product-Mouse Highlighter",
"Product-Mouse Jump",
"Product-Mouse Pointer Crosshairs",
"Product-Mouse Without Borders",
"Product-New+",
"Product-Peek",
"Product-PowerRename",
"Product-PowerToys Run",
"Product-Quick Accent",
"Product-Registry Preview",
"Product-Screen Ruler",
"Product-Settings",
"Product-Shortcut Guide",
"Product-Text Extractor",
"Product-Workspaces",
"Product-ZoomIt",
];
// Map from bug-report "Area(s) with issue?" dropdown values
// to Product-* labels (used as strong hints when the issue body
// contains the area dropdown answer).
const AREA_TO_LABEL = {
"Advanced Paste": "Product-Advanced Paste",
"Always on Top": "Product-Always on Top",
"Awake": "Product-Awake",
"ColorPicker": "Product-ColorPicker",
"Command not found": "Product-Command not found",
"Command Palette": "Product-Command Palette",
"Crop and Lock": "Product-CropAndLock",
"Environment Variables": "Product-Environment Variables",
"FancyZones": "Product-FancyZones",
"FancyZones Editor": "Product-FancyZones",
"File Locksmith": "Product-File Locksmith",
"File Explorer: Preview Pane": "Product-File Explorer",
"File Explorer: Thumbnail preview": "Product-File Explorer",
"Hosts File Editor": "Product-Hosts",
"Image Resizer": "Product-Image Resizer",
"Keyboard Manager": "Product-Keyboard Manager",
"Light Switch": "Product-LightSwitch",
"Mouse Utilities": "Product-Find My Mouse",
"Mouse Without Borders": "Product-Mouse Without Borders",
"New+": "Product-New+",
"Peek": "Product-Peek",
"PowerRename": "Product-PowerRename",
"PowerToys Run": "Product-PowerToys Run",
"Quick Accent": "Product-Quick Accent",
"Registry Preview": "Product-Registry Preview",
"Screen ruler": "Product-Screen Ruler",
"Shortcut Guide": "Product-Shortcut Guide",
"TextExtractor": "Product-Text Extractor",
"Workspaces": "Product-Workspaces",
"ZoomIt": "Product-ZoomIt",
};
// ── Helpers ────────────────────────────────────────────────
function hasProductLabel(labels) {
return labels.some((l) => l.name.startsWith("Product-"));
}
// Try to extract the area from the structured bug-report body
// (the "Area(s) with issue?" dropdown).
function extractAreaFromBody(body) {
if (!body) return null;
// The rendered issue body contains a heading followed by the selected values
const areaMatch = body.match(
/### Area\(s\) with issue\?\s*\n+(.+?)(?:\n###|\n\n|$)/s
);
if (!areaMatch) return null;
const areaText = areaMatch[1].trim();
if (areaText === "_No response_" || areaText === "General") return null;
// Could be comma-separated; take the first specific one
const areas = areaText.split(",").map((a) => a.trim());
for (const area of areas) {
if (AREA_TO_LABEL[area]) return AREA_TO_LABEL[area];
}
return null;
}
// Use GitHub Models to classify an issue when the dropdown area
// is not available or is "General".
const MAX_BODY_LENGTH = 3000; // Truncate body to stay within model token limits while keeping enough context
const MAX_COMPLETION_TOKENS = 60; // Enough for a Product-* label name with some margin
async function classifyWithAI(title, body) {
const truncatedBody = (body || "").slice(0, MAX_BODY_LENGTH);
const labelList = PRODUCT_LABELS.join("\n- ");
const prompt = `You are a GitHub issue triager for the microsoft/PowerToys repository.
Given the issue title and body below, determine which ONE Product label best fits.
Reply with ONLY the label name (e.g. "Product-FancyZones") or "UNKNOWN" if you cannot determine it.
Available labels:
- ${labelList}
Issue title: ${title}
Issue body:
${truncatedBody}`;
try {
const response = await fetch(
"https://models.github.ai/inference/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "openai/gpt-4o",
messages: [{ role: "user", content: prompt }],
max_tokens: MAX_COMPLETION_TOKENS,
temperature: 0,
}),
}
);
if (!response.ok) {
core.warning(`AI classification failed: ${response.status} ${response.statusText}`);
return null;
}
const data = await response.json();
const answer = data.choices?.[0]?.message?.content?.trim();
if (!answer || answer === "UNKNOWN") return null;
// Validate the answer is a known label
if (PRODUCT_LABELS.includes(answer)) return answer;
// Try fuzzy match (the model may include extra text)
const found = PRODUCT_LABELS.find((l) => answer.includes(l));
return found || null;
} catch (err) {
core.warning(`AI classification error: ${err.message}`);
return null;
}
}
// ── Main ───────────────────────────────────────────────────
const MAX_ISSUES = 50; // Process up to 50 issues per run
let labeled = 0;
let skipped = 0;
core.info("Searching for open issues with Needs-Triage but no Product-* label...");
// Paginate through open issues labeled Needs-Triage
for await (const response of github.paginate.iterator(
github.rest.issues.listForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
labels: "Needs-Triage",
sort: "created",
direction: "desc",
per_page: 100,
}
)) {
for (const issue of response.data) {
if (labeled + skipped >= MAX_ISSUES) break;
// Skip pull requests (the API returns them too)
if (issue.pull_request) continue;
if (hasProductLabel(issue.labels)) continue;
core.info(`Processing #${issue.number}: ${issue.title}`);
// 1) Try structured area dropdown first (fast, no AI needed)
let label = extractAreaFromBody(issue.body);
// 2) Fall back to AI classification
if (!label) {
label = await classifyWithAI(issue.title, issue.body);
}
if (label) {
core.info(` → Applying "${label}" to #${issue.number}`);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [label],
});
labeled++;
} else {
core.info(` → Could not determine product label for #${issue.number}, skipping.`);
skipped++;
}
}
if (labeled + skipped >= MAX_ISSUES) break;
}
core.info(`Done. Labeled: ${labeled}, Skipped: ${skipped}`);

View File

@@ -496,23 +496,119 @@ private:
if (!GetGUIThreadInfo(0, &gui_info))
{
Logger::warn(L"Auto-copy: GetGUIThreadInfo failed (error={})", GetLastError());
return false;
}
HWND target = gui_info.hwndFocus ? gui_info.hwndFocus : gui_info.hwndActive;
if (!target)
{
Logger::warn(L"Auto-copy: no focused or active window found");
return false;
}
DWORD_PTR result = 0;
return SendMessageTimeout(target,
WM_COPY,
0,
0,
SMTO_ABORTIFHUNG | SMTO_BLOCK,
50,
&result) != 0;
auto sendResult = SendMessageTimeout(target, WM_COPY, 0, 0, SMTO_ABORTIFHUNG | SMTO_BLOCK, 50, &result);
return sendResult != 0;
}
// Helper: poll clipboard sequence number for a change from initial_sequence.
// Returns true if the sequence number changed within the given number of polls.
bool poll_clipboard_sequence(DWORD initial_sequence, int poll_attempts, std::chrono::milliseconds poll_delay)
{
for (int poll = 0; poll < poll_attempts; ++poll)
{
if (GetClipboardSequenceNumber() != initial_sequence)
{
return true;
}
std::this_thread::sleep_for(poll_delay);
}
return false;
}
// Helper: send Ctrl+C via SendInput, releasing any held modifier keys first
// (the hotkey combination may still have modifiers physically pressed).
bool send_ctrl_c_input()
{
std::vector<INPUT> inputs;
// Release all modifier keys that are currently held down from the hotkey.
// Without this, the target app sees e.g. Win+Shift+Ctrl+C instead of just Ctrl+C.
try_inject_modifier_key_up(inputs, VK_LCONTROL);
try_inject_modifier_key_up(inputs, VK_RCONTROL);
try_inject_modifier_key_up(inputs, VK_LWIN);
try_inject_modifier_key_up(inputs, VK_RWIN);
try_inject_modifier_key_up(inputs, VK_LSHIFT);
try_inject_modifier_key_up(inputs, VK_RSHIFT);
try_inject_modifier_key_up(inputs, VK_LMENU);
try_inject_modifier_key_up(inputs, VK_RMENU);
// Ctrl down
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = VK_CONTROL;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
// C down
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = 0x43; // C
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
// C up
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = 0x43; // C
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
// Ctrl up
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = VK_CONTROL;
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
// Restore modifiers that were held down
try_inject_modifier_key_restore(inputs, VK_LCONTROL);
try_inject_modifier_key_restore(inputs, VK_RCONTROL);
try_inject_modifier_key_restore(inputs, VK_LWIN);
try_inject_modifier_key_restore(inputs, VK_RWIN);
try_inject_modifier_key_restore(inputs, VK_LSHIFT);
try_inject_modifier_key_restore(inputs, VK_RSHIFT);
try_inject_modifier_key_restore(inputs, VK_LMENU);
try_inject_modifier_key_restore(inputs, VK_RMENU);
// Prevent Start Menu from activating after Win key release/restore
INPUT dummyEvent = {};
dummyEvent.type = INPUT_KEYBOARD;
dummyEvent.ki.wVk = 0xFF;
dummyEvent.ki.dwFlags = KEYEVENTF_KEYUP;
inputs.push_back(dummyEvent);
auto uSent = SendInput(static_cast<UINT>(inputs.size()), inputs.data(), sizeof(INPUT));
if (uSent != inputs.size())
{
DWORD errorCode = GetLastError();
auto errorMessage = get_last_error_message(errorCode);
Logger::error(L"SendInput failed for Ctrl+C. Expected to send {} inputs and sent only {}. {}", inputs.size(), uSent, errorMessage.has_value() ? errorMessage.value() : L"");
Trace::AdvancedPaste_Error(errorCode, errorMessage.has_value() ? errorMessage.value() : L"", L"input.SendInput");
return false;
}
return true;
}
bool send_copy_selection()
@@ -526,78 +622,30 @@ private:
for (int attempt = 0; attempt < copy_attempts; ++attempt)
{
const auto initial_sequence = GetClipboardSequenceNumber();
copy_succeeded = try_send_copy_message();
if (!copy_succeeded)
// Strategy 1: Try WM_COPY message (works for standard Win32 controls)
bool wm_copy_sent = try_send_copy_message();
if (wm_copy_sent)
{
std::vector<INPUT> inputs;
// send Ctrl+C (key downs and key ups)
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = VK_CONTROL;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = 0x43; // C
// Avoid triggering detection by the centralized keyboard hook.
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = 0x43; // C
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
// Avoid triggering detection by the centralized keyboard hook.
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
{
INPUT input_event = {};
input_event.type = INPUT_KEYBOARD;
input_event.ki.wVk = VK_CONTROL;
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
inputs.push_back(input_event);
}
auto uSent = SendInput(static_cast<UINT>(inputs.size()), inputs.data(), sizeof(INPUT));
if (uSent != inputs.size())
{
DWORD errorCode = GetLastError();
auto errorMessage = get_last_error_message(errorCode);
Logger::error(L"SendInput failed for Ctrl+C. Expected to send {} inputs and sent only {}. {}", inputs.size(), uSent, errorMessage.has_value() ? errorMessage.value() : L"");
Trace::AdvancedPaste_Error(errorCode, errorMessage.has_value() ? errorMessage.value() : L"", L"input.SendInput");
}
else
if (poll_clipboard_sequence(initial_sequence, clipboard_poll_attempts, clipboard_poll_delay))
{
copy_succeeded = true;
}
}
if (copy_succeeded)
// Strategy 2: If WM_COPY didn't work, try SendInput Ctrl+C (works for Electron, browsers, etc.)
if (!copy_succeeded)
{
bool sequence_changed = false;
for (int poll_attempt = 0; poll_attempt < clipboard_poll_attempts; ++poll_attempt)
const auto sequence_before_ctrl_c = GetClipboardSequenceNumber();
if (send_ctrl_c_input())
{
if (GetClipboardSequenceNumber() != initial_sequence)
if (poll_clipboard_sequence(sequence_before_ctrl_c, clipboard_poll_attempts, clipboard_poll_delay))
{
sequence_changed = true;
break;
copy_succeeded = true;
}
std::this_thread::sleep_for(clipboard_poll_delay);
}
copy_succeeded = sequence_changed;
}
if (copy_succeeded)
@@ -611,6 +659,11 @@ private:
}
}
if (!copy_succeeded)
{
Logger::warn(L"Auto-copy: all {} copy attempts failed — the target application did not update the clipboard after WM_COPY and Ctrl+C", copy_attempts);
}
return copy_succeeded;
}
@@ -977,6 +1030,7 @@ public:
{
if (!send_copy_selection())
{
Logger::warn(L"Auto-copy: failed to copy selection for custom action index {} — aborting action", custom_action_index);
return false;
}
}

View File

@@ -18,7 +18,7 @@ namespace ImageResizer.Models
_scaleFormat ??= CompositeFormat.Parse(ResourceLoaderInstance.GetString("Input_AiScaleFormat"));
[ObservableProperty]
[JsonPropertyName("scale")]
[property: JsonPropertyName("scale")]
private int _scale = 2;
/// <summary>

View File

@@ -26,26 +26,26 @@ namespace ImageResizer.Models
};
[ObservableProperty]
[JsonPropertyName("Id")]
[property: JsonPropertyName("Id")]
private int _id;
private string _name;
[ObservableProperty]
[JsonPropertyName("fit")]
[property: JsonPropertyName("fit")]
[NotifyPropertyChangedFor(nameof(ShowHeight))]
private ResizeFit _fit = ResizeFit.Fit;
[ObservableProperty]
[JsonPropertyName("width")]
[property: JsonPropertyName("width")]
private double _width;
[ObservableProperty]
[JsonPropertyName("height")]
[property: JsonPropertyName("height")]
private double _height;
[ObservableProperty]
[JsonPropertyName("unit")]
[property: JsonPropertyName("unit")]
[NotifyPropertyChangedFor(nameof(ShowHeight))]
private ResizeUnit _unit = ResizeUnit.Pixel;

View File

@@ -471,10 +471,19 @@ namespace PowerDisplay.Common.Drivers.DDC
{
InitializeContrast(monitor, candidate.Handle);
}
// Initialize volume if supported
if (monitor.SupportsVolume)
{
InitializeVolume(monitor, candidate.Handle);
}
}
// Initialize brightness (always supported for DDC/CI monitors)
InitializeBrightness(monitor, candidate.Handle);
// Initialize brightness if supported
if (monitor.SupportsBrightness)
{
InitializeBrightness(monitor, candidate.Handle);
}
monitors.Add(monitor);
newHandleMap[monitor.Id] = candidate.Handle;
@@ -541,6 +550,18 @@ namespace PowerDisplay.Common.Drivers.DDC
}
}
/// <summary>
/// Initialize volume value for a monitor using VCP 0x62.
/// </summary>
private static void InitializeVolume(Monitor monitor, IntPtr handle)
{
if (TryGetVcpFeature(handle, VcpCodeVolume, monitor.Id, out uint current, out uint max))
{
var volumeInfo = new VcpFeatureValue((int)current, 0, (int)max);
monitor.CurrentVolume = volumeInfo.ToPercentage();
}
}
/// <summary>
/// Wrapper for GetVCPFeatureAndVCPFeatureReply that logs errors on failure.
/// </summary>
@@ -568,6 +589,12 @@ namespace PowerDisplay.Common.Drivers.DDC
/// </summary>
private static void UpdateMonitorCapabilitiesFromVcp(Monitor monitor, VcpCapabilities vcpCaps)
{
// Check for Brightness support (VCP 0x10)
if (vcpCaps.SupportsVcpCode(VcpCodeBrightness))
{
monitor.Capabilities |= MonitorCapabilities.Brightness;
}
// Check for Contrast support (VCP 0x12)
if (vcpCaps.SupportsVcpCode(VcpCodeContrast))
{

View File

@@ -165,6 +165,11 @@ namespace PowerDisplay.Common.Models
}
}
/// <summary>
/// Gets a value indicating whether the monitor supports brightness adjustment
/// </summary>
public bool SupportsBrightness => Capabilities.HasFlag(MonitorCapabilities.Brightness);
/// <summary>
/// Gets a value indicating whether the monitor supports contrast adjustment
/// </summary>

View File

@@ -14,28 +14,28 @@ namespace PowerDisplay.Common.Models
public sealed class MonitorStateEntry
{
/// <summary>
/// Gets or sets the brightness level (0-100).
/// Gets or sets the brightness level (0-100), or <c>null</c> if not yet read from the monitor.
/// </summary>
[JsonPropertyName("brightness")]
public int Brightness { get; set; }
public int? Brightness { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP value.
/// Gets or sets the color temperature VCP value, or <c>null</c> if not yet read from the monitor.
/// </summary>
[JsonPropertyName("colorTemperature")]
public int ColorTemperatureVcp { get; set; }
public int? ColorTemperatureVcp { get; set; }
/// <summary>
/// Gets or sets the contrast level (0-100).
/// Gets or sets the contrast level (0-100), or <c>null</c> if not yet read from the monitor.
/// </summary>
[JsonPropertyName("contrast")]
public int Contrast { get; set; }
public int? Contrast { get; set; }
/// <summary>
/// Gets or sets the volume level (0-100).
/// Gets or sets the volume level (0-100), or <c>null</c> if not yet read from the monitor.
/// </summary>
[JsonPropertyName("volume")]
public int Volume { get; set; }
public int? Volume { get; set; }
/// <summary>
/// Gets or sets the raw capabilities string from DDC/CI.

View File

@@ -15,7 +15,7 @@ namespace PowerDisplay.Common.Serialization
/// </summary>
[JsonSourceGenerationOptions(
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
IncludeFields = true)]
[JsonSerializable(typeof(MonitorStateFile))]
[JsonSerializable(typeof(MonitorStateEntry))]

View File

@@ -35,13 +35,13 @@ namespace PowerDisplay.Common.Services
/// </summary>
private sealed class MonitorState
{
public int Brightness { get; set; }
public int? Brightness { get; set; }
public int ColorTemperatureVcp { get; set; }
public int? ColorTemperatureVcp { get; set; }
public int Contrast { get; set; }
public int? Contrast { get; set; }
public int Volume { get; set; }
public int? Volume { get; set; }
public string? CapabilitiesRaw { get; set; }
}
@@ -128,7 +128,7 @@ namespace PowerDisplay.Common.Services
/// </summary>
/// <param name="monitorId">The monitor's unique Id (e.g., "DDC_GSM5C6D_1").</param>
/// <returns>A tuple of (Brightness, ColorTemperatureVcp, Contrast, Volume) or null if not found.</returns>
public (int Brightness, int ColorTemperatureVcp, int Contrast, int Volume)? GetMonitorParameters(string monitorId)
public (int? Brightness, int? ColorTemperatureVcp, int? Contrast, int? Volume)? GetMonitorParameters(string monitorId)
{
if (string.IsNullOrEmpty(monitorId))
{

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using PowerDisplay.Configuration;
using PowerDisplay.Helpers;
@@ -17,6 +17,8 @@ namespace PowerDisplay.PowerDisplayXAML
public sealed partial class IdentifyWindow : WindowEx, IDisposable
{
private DpiSuppressor? _dpiSuppressor;
private DispatcherQueueTimer? _autoCloseTimer;
private bool _disposed;
public IdentifyWindow(string displayText)
{
@@ -40,14 +42,19 @@ namespace PowerDisplay.PowerDisplayXAML
// Ensure DpiSuppressor is disposed when window closes
this.Closed += (_, _) => Dispose();
// Auto close after 3 seconds
Task.Delay(3000).ContinueWith(_ =>
// Auto close after 3 seconds. DispatcherQueueTimer runs on the UI thread
// and can be deterministically cancelled on Dispose, unlike a detached Task.Delay.
_autoCloseTimer = DispatcherQueue.CreateTimer();
_autoCloseTimer.Interval = TimeSpan.FromSeconds(3);
_autoCloseTimer.IsRepeating = false;
_autoCloseTimer.Tick += (_, _) =>
{
DispatcherQueue.TryEnqueue(() =>
if (!_disposed)
{
Close();
});
});
}
};
_autoCloseTimer.Start();
}
private void ConfigureWindow()
@@ -96,6 +103,15 @@ namespace PowerDisplay.PowerDisplayXAML
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_autoCloseTimer?.Stop();
_autoCloseTimer = null;
_dpiSuppressor?.Dispose();
}
}

View File

@@ -25,10 +25,18 @@ namespace PowerDisplay.ViewModels;
/// </summary>
public partial class MainViewModel
{
/// <summary>
/// Check if a value is within the valid range (inclusive).
/// </summary>
private static bool IsValueInRange(int value, int min, int max) => value >= min && value <= max;
private static void TryRestore(
List<Task> tasks,
int? savedValue,
bool isVisible,
int currentValue,
Func<int, Task> setter)
{
if (savedValue.HasValue && isVisible && savedValue.Value != currentValue)
{
tasks.Add(setter(savedValue.Value));
}
}
/// <summary>
/// Apply settings changes from Settings UI (IPC event handler entry point)
@@ -212,32 +220,16 @@ public partial class MainViewModel
}
// Apply brightness if included in profile
if (setting.Brightness.HasValue &&
IsValueInRange(setting.Brightness.Value, monitorVm.MinBrightness, monitorVm.MaxBrightness))
{
updateTasks.Add(monitorVm.SetBrightnessAsync(setting.Brightness.Value));
}
TryRestore(updateTasks, setting.Brightness, monitorVm.ShowBrightness, monitorVm.Brightness, monitorVm.SetBrightnessAsync);
// Apply contrast if supported and value provided
if (setting.Contrast.HasValue && monitorVm.ShowContrast &&
IsValueInRange(setting.Contrast.Value, monitorVm.MinContrast, monitorVm.MaxContrast))
{
updateTasks.Add(monitorVm.SetContrastAsync(setting.Contrast.Value));
}
TryRestore(updateTasks, setting.Contrast, monitorVm.ShowContrast, monitorVm.Contrast, monitorVm.SetContrastAsync);
// Apply volume if supported and value provided
if (setting.Volume.HasValue && monitorVm.ShowVolume &&
IsValueInRange(setting.Volume.Value, monitorVm.MinVolume, monitorVm.MaxVolume))
{
updateTasks.Add(monitorVm.SetVolumeAsync(setting.Volume.Value));
}
TryRestore(updateTasks, setting.Volume, monitorVm.ShowVolume, monitorVm.Volume, monitorVm.SetVolumeAsync);
// Apply color temperature if included in profile
if (setting.ColorTemperatureVcp.HasValue && setting.ColorTemperatureVcp.Value > 0 &&
monitorVm.ShowColorTemperature)
{
updateTasks.Add(monitorVm.SetColorTemperatureAsync(setting.ColorTemperatureVcp.Value));
}
TryRestore(updateTasks, setting.ColorTemperatureVcp, monitorVm.ShowColorTemperature, monitorVm.ColorTemperature, monitorVm.SetColorTemperatureAsync);
}
// Wait for all updates to complete
@@ -266,36 +258,12 @@ public partial class MainViewModel
continue;
}
// Restore brightness if different from current
if (IsValueInRange(savedState.Value.Brightness, monitorVm.MinBrightness, monitorVm.MaxBrightness) &&
savedState.Value.Brightness != monitorVm.Brightness)
{
updateTasks.Add(monitorVm.SetBrightnessAsync(savedState.Value.Brightness));
}
var (brightness, colorTemp, contrast, volume) = savedState.Value;
// Restore color temperature if different from current
if (monitorVm.ShowColorTemperature &&
savedState.Value.ColorTemperatureVcp > 0 &&
savedState.Value.ColorTemperatureVcp != monitorVm.ColorTemperature)
{
updateTasks.Add(monitorVm.SetColorTemperatureAsync(savedState.Value.ColorTemperatureVcp));
}
// Restore contrast if different from current
if (monitorVm.ShowContrast &&
IsValueInRange(savedState.Value.Contrast, monitorVm.MinContrast, monitorVm.MaxContrast) &&
savedState.Value.Contrast != monitorVm.Contrast)
{
updateTasks.Add(monitorVm.SetContrastAsync(savedState.Value.Contrast));
}
// Restore volume if different from current
if (monitorVm.ShowVolume &&
IsValueInRange(savedState.Value.Volume, monitorVm.MinVolume, monitorVm.MaxVolume) &&
savedState.Value.Volume != monitorVm.Volume)
{
updateTasks.Add(monitorVm.SetVolumeAsync(savedState.Value.Volume));
}
TryRestore(updateTasks, brightness, monitorVm.ShowBrightness, monitorVm.Brightness, monitorVm.SetBrightnessAsync);
TryRestore(updateTasks, colorTemp, monitorVm.ShowColorTemperature, monitorVm.ColorTemperature, monitorVm.SetColorTemperatureAsync);
TryRestore(updateTasks, contrast, monitorVm.ShowContrast, monitorVm.Contrast, monitorVm.SetContrastAsync);
TryRestore(updateTasks, volume, monitorVm.ShowVolume, monitorVm.Volume, monitorVm.SetVolumeAsync);
}
if (updateTasks.Count > 0)

View File

@@ -228,10 +228,13 @@ public partial class MainViewModel : ObservableObject, IDisposable
// Format display text: single number for normal mode, "1|2" for mirror mode
var displayText = string.Join("|", monitorNumbers);
// Create and position identify window
// Create and position identify window.
// Position before Activate so the window appears directly at the target
// location — avoiding a visible flicker from the default spawn position
// and skipping a WM_DPICHANGED round-trip when crossing DPI monitors.
var identifyWindow = new IdentifyWindow(displayText);
identifyWindow.Activate();
identifyWindow.PositionOnDisplay(displayArea);
identifyWindow.Activate();
windowsCreated++;
}
}

View File

@@ -41,6 +41,9 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
public partial bool IsAvailable { get; set; }
// Visibility settings (controlled by Settings UI)
[ObservableProperty]
public partial bool ShowBrightness { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasAdvancedControls))]
public partial bool ShowContrast { get; set; }
@@ -111,12 +114,32 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
await ApplyPropertyToHardwareAsync(nameof(Volume), volume, _monitorManager.SetVolumeAsync);
}
private bool IsDiscreteValueSupported(byte vcpCode, int value)
{
var vcpInfo = VcpCapabilitiesInfo;
if (vcpInfo == null ||
!vcpInfo.SupportedVcpCodes.TryGetValue(vcpCode, out var codeInfo) ||
!codeInfo.HasDiscreteValues ||
!codeInfo.SupportedValues.Contains(value))
{
Logger.LogWarning($"[{Id}] VCP 0x{vcpCode:X2} value 0x{value:X2} not in reported supported values, skipping");
return false;
}
return true;
}
/// <summary>
/// Unified method to apply color temperature with hardware update and state persistence.
/// Always immediate (no debouncing for discrete preset values).
/// </summary>
public async Task SetColorTemperatureAsync(int colorTemperature)
{
if (!IsDiscreteValueSupported(0x14, colorTemperature))
{
return;
}
try
{
var result = await _monitorManager.SetColorTemperatureAsync(Id, colorTemperature);
@@ -193,6 +216,7 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
_monitor.PropertyChanged += OnMonitorPropertyChanged;
// Initialize Show properties based on hardware capabilities
ShowBrightness = monitor.SupportsBrightness;
ShowContrast = monitor.SupportsContrast;
ShowVolume = monitor.SupportsVolume;
ShowInputSource = monitor.SupportsInputSource;
@@ -272,6 +296,8 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
/// </summary>
public bool SupportsContrast => _monitor.SupportsContrast;
public bool SupportsBrightness => _monitor.SupportsBrightness;
/// <summary>
/// Gets a value indicating whether this monitor supports volume control via VCP 0x62
/// </summary>
@@ -458,41 +484,23 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
}
/// <summary>
/// Standard MCCS color temperature presets (VCP 0x14 values) to use as fallback
/// when the monitor doesn't report discrete values in its capabilities string.
/// </summary>
private static readonly int[] StandardColorTemperaturePresets = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08, 0x09, 0x0A, 0x0B };
/// <summary>
/// Refresh the list of available color temperature presets based on monitor capabilities
/// Refresh the list of available color temperature presets based on monitor capabilities.
/// Only values explicitly reported in the capabilities string are exposed — no MCCS standard fallback.
/// </summary>
private void RefreshAvailableColorPresets()
{
if (!SupportsColorTemperature)
var vcpInfo = VcpCapabilitiesInfo;
if (!SupportsColorTemperature ||
vcpInfo == null ||
!vcpInfo.SupportedVcpCodes.TryGetValue(0x14, out var colorTempInfo) ||
!colorTempInfo.HasDiscreteValues)
{
_availableColorPresets = null;
OnPropertyChanged(nameof(AvailableColorPresets));
return;
}
IEnumerable<int> presetValues;
var vcpInfo = VcpCapabilitiesInfo;
// Try to get discrete values from capabilities string
if (vcpInfo != null &&
vcpInfo.SupportedVcpCodes.TryGetValue(0x14, out var colorTempInfo) &&
colorTempInfo.HasDiscreteValues &&
colorTempInfo.SupportedValues.Count > 0)
{
// Use values from capabilities string
presetValues = colorTempInfo.SupportedValues;
}
else
{
// Fallback to standard MCCS presets when capabilities don't list discrete values
presetValues = StandardColorTemperaturePresets;
}
_availableColorPresets = presetValues.Select(value => new ColorTemperatureItem
_availableColorPresets = colorTempInfo.SupportedValues.Select(value => new ColorTemperatureItem
{
VcpValue = value,
DisplayName = Common.Utils.VcpNames.GetValueName(0x14, value, _mainViewModel?.CustomVcpMappings, _monitor.Id) is string n ? $"{n} (0x{value:X2})" : $"0x{value:X2}",
@@ -584,6 +592,11 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
/// </summary>
public async Task SetInputSourceAsync(int inputSource)
{
if (!IsDiscreteValueSupported(0x60, inputSource))
{
return;
}
try
{
var result = await _monitorManager.SetInputSourceAsync(Id, inputSource);
@@ -670,6 +683,11 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
/// </summary>
public async Task SetPowerStateAsync(int powerState)
{
if (!IsDiscreteValueSupported(0xD6, powerState))
{
return;
}
try
{
var result = await _monitorManager.SetPowerStateAsync(Id, powerState);

View File

@@ -119,7 +119,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool powerLauncher = true;
private bool powerLauncher; // defaulting to off
[JsonPropertyName("PowerToys Run")]
public bool PowerLauncher
@@ -153,7 +153,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool cropAndLock = true;
private bool cropAndLock; // defaulting to off
[JsonPropertyName("CropAndLock")]
public bool CropAndLock
@@ -315,7 +315,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool advancedPaste = true;
private bool advancedPaste; // defaulting to off
[JsonPropertyName("AdvancedPaste")]
public bool AdvancedPaste
@@ -349,7 +349,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool hosts = true;
private bool hosts; // defaulting to off
[JsonPropertyName("Hosts")]
public bool Hosts
@@ -398,7 +398,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool registryPreview = true;
private bool registryPreview; // defaulting to off
[JsonPropertyName("RegistryPreview")]
public bool RegistryPreview
@@ -431,7 +431,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool environmentVariables = true;
private bool environmentVariables; // defaulting to off
[JsonPropertyName("EnvironmentVariables")]
public bool EnvironmentVariables
@@ -463,7 +463,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool workspaces = true;
private bool workspaces; // defaulting to off
[JsonPropertyName("Workspaces")]
public bool Workspaces

View File

@@ -351,7 +351,7 @@ namespace ViewModelTests
Assert.IsTrue(modules.PowerPreview);
Assert.IsTrue(modules.ShortcutGuide);
Assert.IsTrue(modules.PowerRename);
Assert.IsTrue(modules.PowerLauncher);
Assert.IsFalse(modules.PowerLauncher);
Assert.IsTrue(modules.ColorPicker);
}
}