161 lines
6.1 KiB
QML
161 lines
6.1 KiB
QML
// The resolution list for one display.
|
|
//
|
|
// 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
|
|
|
|
Column {
|
|
id: root
|
|
|
|
property var monitor: null
|
|
property bool enabled: true
|
|
|
|
// 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: {
|
|
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]);
|
|
}
|
|
|
|
// 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
|
|
|
|
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
|
|
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
|
|
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: 10
|
|
|
|
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 {
|
|
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)
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|