Rebuild Displays around the canvas, and let the transaction keep color
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
// 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.
|
||||
// One line per resolution, not per mode: this panel reports 35 modes, most of
|
||||
// which differ only in refresh-rate rounding, and a flat list of
|
||||
// "[email protected]" strings is a wall of near-identical text rather than a
|
||||
// choice. The rates live in their own row on the page, where changing only the
|
||||
// rate does not mean going back through the resolution you already had.
|
||||
//
|
||||
// Each line carries the two facts that decide the choice: the shape of the
|
||||
// picture, and whether this is the one the panel was built for.
|
||||
//
|
||||
// Applying is the page's job. Every per-display edit on the Displays page goes
|
||||
// through one funnel so the whole record -- position, colour, mirror state --
|
||||
// rides the same keep-or-revert transaction; this reports which resolution was
|
||||
// asked for and lets that funnel do the rest.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
@@ -15,9 +23,10 @@ Column {
|
||||
property var monitor: null
|
||||
property bool enabled: true
|
||||
|
||||
// Emitted once a mode has been asked for, so a container can put the list
|
||||
// away. Applying is still this component's job; closing is not.
|
||||
signal picked
|
||||
// The mode string for the resolution that was chosen, at the rate closest
|
||||
// to the one in use.
|
||||
signal picked(string mode)
|
||||
|
||||
spacing: 0
|
||||
|
||||
readonly property var grouped: {
|
||||
@@ -36,6 +45,44 @@ Column {
|
||||
return order.map(key => buckets[key]);
|
||||
}
|
||||
|
||||
// Modes arrive sorted by area, so the first is the largest the panel
|
||||
// advertises -- which is the one it was built for.
|
||||
readonly property string nativeLabel: root.grouped.length > 0 ? root.grouped[0].label : ""
|
||||
|
||||
function aspectLabel(width: int, height: int): string {
|
||||
if (!width || !height)
|
||||
return "";
|
||||
const ratio = width / height;
|
||||
const named = [
|
||||
{ ratio: 1, label: "1:1" },
|
||||
{ ratio: 5 / 4, label: "5:4" },
|
||||
{ ratio: 4 / 3, label: "4:3" },
|
||||
{ ratio: 3 / 2, label: "3:2" },
|
||||
{ ratio: 16 / 10, label: "16:10" },
|
||||
{ ratio: 16 / 9, label: "16:9" },
|
||||
{ ratio: 21 / 9, label: "21:9" },
|
||||
{ ratio: 32 / 9, label: "32:9" }
|
||||
];
|
||||
for (const entry of named) {
|
||||
if (Math.abs(ratio - entry.ratio) < 0.05)
|
||||
return entry.label;
|
||||
}
|
||||
const reduce = (a, b) => b === 0 ? a : reduce(b, a % b);
|
||||
const divisor = reduce(width, height) || 1;
|
||||
return `${Math.round(width / divisor)}:${Math.round(height / divisor)}`;
|
||||
}
|
||||
|
||||
// The rate nearest the one in use, so changing the resolution does not
|
||||
// quietly change the refresh rate as well when the panel offers the same
|
||||
// one at the new size.
|
||||
function modeFor(group: var): string {
|
||||
const target = root.monitor ? root.monitor.refreshRate : 0;
|
||||
const rates = group.rates.slice().sort((left, right) =>
|
||||
Math.abs(left.refresh - target) - Math.abs(right.refresh - target)
|
||||
|| right.refresh - left.refresh);
|
||||
return rates.length > 0 ? rates[0].mode : "";
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.grouped
|
||||
|
||||
@@ -45,79 +92,66 @@ Column {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool isCurrent: root.monitor
|
||||
readonly property bool isCurrent: !!root.monitor
|
||||
&& root.monitor.width === resolution.modelData.width
|
||||
&& root.monitor.height === resolution.modelData.height
|
||||
readonly property bool isNative: resolution.modelData.label === root.nativeLabel
|
||||
readonly property string tag: {
|
||||
if (resolution.isCurrent && resolution.isNative)
|
||||
return "Current · Native";
|
||||
if (resolution.isCurrent)
|
||||
return "Current";
|
||||
return resolution.isNative ? "Native" : "";
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
label: resolution.modelData.label
|
||||
detail: resolution.isCurrent ? "Current resolution" : ""
|
||||
controlWidth: Math.max(120, resolution.modelData.rates.length * 84)
|
||||
controlWidth: 170
|
||||
divider: resolution.index < root.grouped.length - 1
|
||||
opacity: root.enabled ? 1 : 0.45
|
||||
activatable: root.enabled && !resolution.isCurrent
|
||||
onActivated: {
|
||||
const mode = root.modeFor(resolution.modelData);
|
||||
if (mode !== "")
|
||||
root.picked(mode);
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
spacing: 10
|
||||
|
||||
Repeater {
|
||||
model: resolution.modelData.rates
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.aspectLabel(resolution.modelData.width, resolution.modelData.height)
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rate
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: resolution.tag !== ""
|
||||
width: visible ? tagLabel.implicitWidth + 18 : 0
|
||||
height: 21
|
||||
radius: Theme.pillRadius
|
||||
color: resolution.isCurrent
|
||||
? Theme.alpha(Theme.accent, 0.1)
|
||||
: Theme.alpha(Theme.fg, 0.06)
|
||||
border.width: 1
|
||||
border.color: resolution.isCurrent
|
||||
? Theme.alpha(Theme.accent, 0.25)
|
||||
: Theme.alpha(Theme.fg, 0.1)
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property bool selected: resolution.isCurrent
|
||||
&& Displays.modeIsCurrent(root.monitor, rate.modelData)
|
||||
|
||||
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,
|
||||
Displays.nearestCleanScale(rate.modelData.mode, root.monitor.scale),
|
||||
root.monitor.transform);
|
||||
root.picked();
|
||||
}
|
||||
}
|
||||
Text {
|
||||
id: tagLabel
|
||||
anchors.centerIn: parent
|
||||
text: resolution.tag
|
||||
color: resolution.isCurrent ? Theme.accentAlt : Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user