Make displays configurable, with a revert countdown
Resolution, refresh rate, scale, and rotation, applied through
hl.monitor{} and stored per output.
This is the only setting in Panama where a wrong value can leave the
user unable to SEE the screen well enough to undo it: a mode the panel
cannot show, or a scale that makes everything unreadable, is not
recoverable through the UI that caused it. So a change is never applied
irreversibly. It is applied, then reverted automatically after fifteen
seconds unless confirmed, and confirming is what writes it to the
settings store -- letting the countdown run leaves nothing behind.
The contract tests that property specifically: it applies a scale, waits
out the countdown, and asserts the display came back and that nothing
was stored. A regression there is not a broken feature, it is a user
staring at a blank monitor.
Modes are grouped by resolution with refresh rates beside them. The
panel reports 35, many differing only in refresh-rate rounding -- 60.00
and 59.94 -- which as a flat list of buttons is noise rather than
choice; equal rounded pairs collapse, leaving 21.
Only mode, scale, and transform are configurable. Colour management and
bit depth stay in monitors.lua because they carry a documented screencopy
tradeoff that a settings page cannot explain at the moment you would be
changing it.
Also replaces the display policy rows with the schema-bound ones, so the
page no longer restates labels that PreferenceSchema already holds.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// The resolution list for one display.
|
||||
//
|
||||
// Grouped by resolution with refresh rates beside it, rather than a flat list
|
||||
// of "[email protected]" strings: this panel reports 35 modes, many of which
|
||||
// differ only in refresh-rate rounding, and a flat list of those is a wall of
|
||||
// near-identical text rather than a choice.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property var monitor: null
|
||||
property bool enabled: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
readonly property var grouped: {
|
||||
if (!root.monitor)
|
||||
return [];
|
||||
const buckets = {};
|
||||
const order = [];
|
||||
for (const mode of root.monitor.modes) {
|
||||
const key = mode.label;
|
||||
if (!buckets[key]) {
|
||||
buckets[key] = { label: key, width: mode.width, height: mode.height, rates: [] };
|
||||
order.push(key);
|
||||
}
|
||||
buckets[key].rates.push(mode);
|
||||
}
|
||||
return order.map(key => buckets[key]);
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.grouped
|
||||
|
||||
SettingRow {
|
||||
id: resolution
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool isCurrent: root.monitor
|
||||
&& root.monitor.width === resolution.modelData.width
|
||||
&& root.monitor.height === resolution.modelData.height
|
||||
|
||||
label: resolution.modelData.label
|
||||
detail: resolution.isCurrent ? "Current resolution" : ""
|
||||
controlWidth: Math.max(120, resolution.modelData.rates.length * 84)
|
||||
divider: resolution.index < root.grouped.length - 1
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: resolution.modelData.rates
|
||||
|
||||
Rectangle {
|
||||
id: rate
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property bool selected: root.monitor
|
||||
&& resolution.isCurrent
|
||||
&& Math.round(root.monitor.refreshRate) === rate.modelData.refresh
|
||||
|
||||
implicitWidth: Math.max(74, rateCaption.implicitWidth + 22)
|
||||
implicitHeight: 30
|
||||
radius: 9
|
||||
opacity: root.enabled ? 1 : 0.45
|
||||
color: rate.selected ? "transparent" : Theme.alpha(Theme.fg, rateHover.hovered && root.enabled ? 0.11 : 0.06)
|
||||
border.width: rate.selected ? 1 : 0
|
||||
border.color: Theme.alpha(Theme.accent, 0.5)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
visible: rate.selected
|
||||
border.width: 0
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: Theme.alpha(Theme.accent, 0.28) }
|
||||
GradientStop { position: 1.0; color: Theme.alpha(Theme.accentSecondary, 0.28) }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: rateCaption
|
||||
anchors.centerIn: parent
|
||||
text: rate.modelData.refreshLabel
|
||||
color: rate.selected ? Theme.fg : Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: rate.selected ? Font.DemiBold : Font.Normal
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: rateHover
|
||||
enabled: root.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: root.enabled && !rate.selected
|
||||
onTapped: Displays.apply(
|
||||
root.monitor.name,
|
||||
rate.modelData.mode,
|
||||
root.monitor.scale,
|
||||
root.monitor.transform)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user