mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-05 09:59:28 +02:00
Compare commits
7 Commits
legendaryb
...
copilot/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c412e85cfc | ||
|
|
049cbccd21 | ||
|
|
4308eb65a9 | ||
|
|
5520ae4cfa | ||
|
|
beddc3b065 | ||
|
|
088da21a70 | ||
|
|
befb5c672e |
232
.github/workflows/scheduled-issue-labeling.yml
vendored
Normal file
232
.github/workflows/scheduled-issue-labeling.yml
vendored
Normal 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}`);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user