Verify display restoration and expose outputs
This commit is contained in:
@@ -16,7 +16,8 @@ ShellRoot {
|
||||
name: monitor ? monitor.name : "",
|
||||
width: monitor ? monitor.width : 0,
|
||||
height: monitor ? monitor.height : 0,
|
||||
refresh: monitor ? Math.round(monitor.refreshRate) : 0,
|
||||
refresh: monitor ? monitor.refreshRate : 0,
|
||||
mode: monitor ? monitor.mode : "",
|
||||
scale: monitor ? monitor.scale : 0,
|
||||
transform: monitor ? monitor.transform : -1,
|
||||
modes: monitor ? monitor.modes.length : 0,
|
||||
@@ -31,14 +32,14 @@ ShellRoot {
|
||||
function applyScale(scale: real): bool {
|
||||
const monitor = Displays.monitors[0];
|
||||
if (!monitor) return false;
|
||||
const mode = monitor.width + "x" + monitor.height + "@" + Math.round(monitor.refreshRate);
|
||||
const mode = monitor.mode;
|
||||
return Displays.apply(monitor.name, mode, scale, monitor.transform);
|
||||
}
|
||||
|
||||
function applyBad(kind: string): bool {
|
||||
const monitor = Displays.monitors[0];
|
||||
if (!monitor) return false;
|
||||
const mode = monitor.width + "x" + monitor.height + "@" + Math.round(monitor.refreshRate);
|
||||
const mode = monitor.mode;
|
||||
if (kind === "mode") return Displays.apply(monitor.name, "9999x9999@240", monitor.scale, monitor.transform);
|
||||
if (kind === "scale") return Displays.apply(monitor.name, mode, 1.37, monitor.transform);
|
||||
if (kind === "dirtyScale") {
|
||||
|
||||
@@ -20,11 +20,24 @@ SettingsPage {
|
||||
title: "Displays"
|
||||
lede: SystemSettings.monitorDescription || "Reading the active display…"
|
||||
|
||||
readonly property var monitor: Displays.monitors.length > 0 ? Displays.monitors[0] : null
|
||||
property string selectedOutput: ""
|
||||
readonly property var monitor: Displays.monitorNamed(root.selectedOutput)
|
||||
?? (Displays.monitors.length > 0 ? Displays.monitors[0] : null)
|
||||
readonly property string currentMode: root.monitor
|
||||
? `${root.monitor.width}x${root.monitor.height}@${Math.round(root.monitor.refreshRate)}`
|
||||
? root.monitor.mode
|
||||
: ""
|
||||
|
||||
function syncSelectedOutput(): void {
|
||||
if (!Displays.monitorNamed(root.selectedOutput))
|
||||
root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : "";
|
||||
}
|
||||
|
||||
Component.onCompleted: root.syncSelectedOutput()
|
||||
Connections {
|
||||
target: Displays
|
||||
function onMonitorsChanged(): void { root.syncSelectedOutput(); }
|
||||
}
|
||||
|
||||
// The confirmation sits above everything, because while it is counting down
|
||||
// it is the only thing that matters on this page.
|
||||
header: Component {
|
||||
@@ -86,6 +99,25 @@ SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
visible: Displays.monitors.length > 1
|
||||
title: "Connected display"
|
||||
subtitle: "Choose the output whose resolution, scale, and rotation you want to adjust."
|
||||
|
||||
ChoiceGrid {
|
||||
width: parent.width
|
||||
label: "Display"
|
||||
options: Displays.monitors.map(monitor => ({
|
||||
value: monitor.name,
|
||||
label: monitor.description || monitor.name
|
||||
}))
|
||||
current: root.monitor ? root.monitor.name : ""
|
||||
enabled: !Displays.awaitingConfirmation && !Displays.busy
|
||||
divider: false
|
||||
onPicked: value => root.selectedOutput = value
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: root.monitor ? root.monitor.name : (SystemSettings.monitorName || "Active display")
|
||||
subtitle: root.monitor
|
||||
|
||||
@@ -36,6 +36,8 @@ Singleton {
|
||||
property var pendingRequested: null
|
||||
property bool pendingVerified: false
|
||||
property bool revertQueued: false
|
||||
property var revertExpected: null
|
||||
property string revertReason: ""
|
||||
property int secondsLeft: 0
|
||||
|
||||
readonly property bool awaitingConfirmation: root.pendingOutput !== ""
|
||||
@@ -43,6 +45,7 @@ Singleton {
|
||||
&& root.pendingVerified
|
||||
&& !root.busy
|
||||
readonly property bool busy: query.running || applyRun.running || revertRun.running
|
||||
|| root.revertExpected !== null
|
||||
|
||||
readonly property int confirmSeconds: 15
|
||||
|
||||
@@ -91,9 +94,10 @@ Singleton {
|
||||
Process {
|
||||
id: revertRun
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode !== 0)
|
||||
root.lastError = "The previous display setting could not be restored automatically.";
|
||||
root.refresh();
|
||||
// Exit status is advisory only. Hyprland's Lua bridge can report
|
||||
// success without applying a value, so exact readback decides.
|
||||
revertVerifyTimer.attempts = 0;
|
||||
revertVerifyTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,22 +111,42 @@ Singleton {
|
||||
function parse(text: string): void {
|
||||
try {
|
||||
const raw = JSON.parse(text);
|
||||
root.monitors = raw.map(monitor => ({
|
||||
name: monitor.name ?? "",
|
||||
description: monitor.description ?? monitor.model ?? "Display",
|
||||
width: monitor.width ?? 0,
|
||||
height: monitor.height ?? 0,
|
||||
refreshRate: monitor.refreshRate ?? 0,
|
||||
scale: monitor.scale ?? 1,
|
||||
transform: monitor.transform ?? 0,
|
||||
modes: root.normaliseModes(monitor.availableModes ?? [])
|
||||
}));
|
||||
root.monitors = raw.map(monitor => {
|
||||
const modes = root.normaliseModes(monitor.availableModes ?? []);
|
||||
const width = monitor.width ?? 0;
|
||||
const height = monitor.height ?? 0;
|
||||
const refreshRate = monitor.refreshRate ?? 0;
|
||||
const current = modes
|
||||
.filter(mode => mode.width === width && mode.height === height)
|
||||
.sort((left, right) =>
|
||||
Math.abs(left.refresh - refreshRate) - Math.abs(right.refresh - refreshRate))[0];
|
||||
return {
|
||||
name: monitor.name ?? "",
|
||||
description: monitor.description ?? monitor.model ?? "Display",
|
||||
width: width,
|
||||
height: height,
|
||||
refreshRate: refreshRate,
|
||||
mode: current?.mode ?? `${width}x${height}@${refreshRate}`,
|
||||
scale: monitor.scale ?? 1,
|
||||
transform: monitor.transform ?? 0,
|
||||
modes: modes
|
||||
};
|
||||
});
|
||||
if (root.awaitingConfirmation && root.pendingRequested
|
||||
&& root.matchesRequest(root.monitorNamed(root.pendingOutput), root.pendingRequested)) {
|
||||
root.pendingVerified = true;
|
||||
verifyTimer.stop();
|
||||
root.lastError = "";
|
||||
} else if (!root.awaitingConfirmation && (
|
||||
} else if (root.revertExpected
|
||||
&& root.matchesRequest(root.monitorNamed(root.revertExpected.output), root.revertExpected)) {
|
||||
revertVerifyTimer.stop();
|
||||
root.revertExpected = null;
|
||||
if (root.revertReason === "")
|
||||
root.lastError = "";
|
||||
else
|
||||
root.lastError = root.revertReason;
|
||||
root.revertReason = "";
|
||||
} else if (!root.awaitingConfirmation && !root.revertExpected && (
|
||||
root.lastError === "Could not read the connected displays."
|
||||
|| root.lastError === "The display list could not be read.")) {
|
||||
root.lastError = "";
|
||||
@@ -133,9 +157,9 @@ Singleton {
|
||||
}
|
||||
|
||||
// "[email protected]" -> a sortable record. The compositor reports the same
|
||||
// resolution several times at refresh rates that differ only in rounding
|
||||
// (60.00 and 59.94), which as a list of buttons is noise rather than
|
||||
// choice, so equal rounded pairs collapse to one.
|
||||
// resolution at distinct rates such as 60.00 and 59.94. Those identities
|
||||
// remain separate because confirmation and recovery must read back the
|
||||
// exact mode the user chose, even when their rounded labels look similar.
|
||||
function normaliseModes(raw: var): var {
|
||||
const seen = {};
|
||||
const out = [];
|
||||
@@ -145,15 +169,19 @@ Singleton {
|
||||
continue;
|
||||
const width = Number(match[1]);
|
||||
const height = Number(match[2]);
|
||||
const refresh = Math.round(Number(match[3]));
|
||||
const key = `${width}x${height}@${refresh}`;
|
||||
const refreshText = match[3];
|
||||
const refresh = Number(refreshText);
|
||||
const roundedRefresh = Math.round(refresh);
|
||||
const key = `${width}x${height}@${refreshText}`;
|
||||
if (seen[key])
|
||||
continue;
|
||||
seen[key] = true;
|
||||
out.push({
|
||||
label: `${width} × ${height}`,
|
||||
refreshLabel: `${refresh} Hz`,
|
||||
mode: key,
|
||||
refreshLabel: Math.abs(refresh - roundedRefresh) < 0.005
|
||||
? `${roundedRefresh} Hz`
|
||||
: `${refresh.toFixed(2)} Hz`,
|
||||
mode: `${width}x${height}@${refreshText}`,
|
||||
width: width,
|
||||
height: height,
|
||||
refresh: refresh
|
||||
@@ -207,7 +235,7 @@ Singleton {
|
||||
return !!parts
|
||||
&& monitor.width === parts.width
|
||||
&& monitor.height === parts.height
|
||||
&& Math.abs(monitor.refreshRate - parts.refresh) < 0.6
|
||||
&& Math.abs(monitor.refreshRate - parts.refresh) < 0.01
|
||||
&& Math.abs(monitor.scale - requested.scale) < 0.001
|
||||
&& monitor.transform === requested.transform;
|
||||
}
|
||||
@@ -215,6 +243,10 @@ Singleton {
|
||||
// Applies immediately and starts the countdown. Nothing is stored yet: the
|
||||
// settings file is only written by confirm().
|
||||
function apply(output: string, mode: string, scale: real, transform: int): bool {
|
||||
if (root.busy) {
|
||||
root.lastError = "Wait for the current display operation to finish.";
|
||||
return false;
|
||||
}
|
||||
if (root.awaitingConfirmation) {
|
||||
root.lastError = "Finish the current display change first.";
|
||||
return false;
|
||||
@@ -239,7 +271,7 @@ Singleton {
|
||||
|
||||
root.pendingPrevious = {
|
||||
output: output,
|
||||
mode: `${monitor.width}x${monitor.height}@${Math.round(monitor.refreshRate)}`,
|
||||
mode: monitor.mode,
|
||||
scale: monitor.scale,
|
||||
transform: monitor.transform
|
||||
};
|
||||
@@ -313,6 +345,7 @@ Singleton {
|
||||
countdown.stop();
|
||||
verifyTimer.stop();
|
||||
root.pendingVerified = false;
|
||||
root.revertReason = message;
|
||||
if (message !== "")
|
||||
root.lastError = message;
|
||||
if (applyRun.running) {
|
||||
@@ -324,6 +357,7 @@ Singleton {
|
||||
|
||||
function performRevert(): void {
|
||||
const previous = root.pendingPrevious;
|
||||
root.revertExpected = previous;
|
||||
root.clearPending();
|
||||
if (previous) {
|
||||
revertRun.exec(["hyprctl", "eval",
|
||||
@@ -362,6 +396,24 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: revertVerifyTimer
|
||||
property int attempts: 0
|
||||
interval: 120
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
attempts++;
|
||||
if (attempts > 25) {
|
||||
stop();
|
||||
root.revertExpected = null;
|
||||
root.revertReason = "";
|
||||
root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually.";
|
||||
return;
|
||||
}
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: countdown
|
||||
interval: 1000
|
||||
|
||||
Reference in New Issue
Block a user