diff options
| -rw-r--r-- | desktop/modules/monitors/MonitorsPage.qml | 250 |
1 files changed, 247 insertions, 3 deletions
diff --git a/desktop/modules/monitors/MonitorsPage.qml b/desktop/modules/monitors/MonitorsPage.qml index 3df7b1c..df13d92 100644 --- a/desktop/modules/monitors/MonitorsPage.qml +++ b/desktop/modules/monitors/MonitorsPage.qml @@ -10,14 +10,258 @@ // GNU General Public License for more details. import QtQuick +import QtQuick.Controls as C import "../.." +import "../appearance" as A +import "Mon.js" as M +// The keep-or-revert bar while an Apply is live, the layout drawn to scale +// (drag a monitor to move it; it snaps to the other's edges), and one card +// per monitor: mode, scale, rotation, VRR, position. Column { id: page required property QtObject mod + spacing: 14 + + // Leaving the page with an Apply unanswered is a Revert. + Component.onDestruction: if (page.mod.pending) page.mod.revert() + + // The stock ComboBox paints from the palette, light by default; these + // are the desktop's colours, as in qtmaildir's TagsKeysPage. + component Combo: C.ComboBox { + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + palette { + base: Theme.surface; window: Theme.base; button: Theme.surface + text: Theme.text; buttonText: Theme.text; windowText: Theme.text + highlight: Theme.accent; highlightedText: Theme.base + mid: Qt.alpha(Theme.text, 0.2); dark: Qt.alpha(Theme.text, 0.2); light: Theme.surface + } + } + + component Label: Text { + width: 90 + anchors.verticalCenter: parent.verticalCenter + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.subtext + } + + Rectangle { + visible: page.mod.pending + width: parent.width + height: 44 + radius: 10 + color: Qt.alpha(Theme.yellow, 0.18) + border.width: 1 + border.color: Qt.alpha(Theme.yellow, 0.5) + Text { + anchors { left: parent.left; leftMargin: 14; verticalCenter: parent.verticalCenter } + text: "Keep these settings? Reverting in " + page.mod.countdown + "s" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.text + } + Row { + anchors { right: parent.right; rightMargin: 10; verticalCenter: parent.verticalCenter } + spacing: 8 + Button { text: "Revert"; onClicked: page.mod.revert() } + Button { text: "Keep"; onClicked: page.mod.keep() } + } + } + + // ---- layout ---- + Rectangle { + id: canvas + width: parent.width + height: 260 + radius: 10 + color: Theme.surface + + // Logical rectangles, and the fit of their bounds into the canvas. + readonly property var rects: page.mod.draft.map(d => { + const p = M.xy(d), s = M.size(d.mode, d.scale, d.transform); + return { x: p.x, y: p.y, w: s.w, h: s.h }; + }) + readonly property real minX: Math.min.apply(null, rects.map(r => r.x).concat([0])) + readonly property real minY: Math.min.apply(null, rects.map(r => r.y).concat([0])) + readonly property real maxX: Math.max.apply(null, rects.map(r => r.x + r.w).concat([1])) + readonly property real maxY: Math.max.apply(null, rects.map(r => r.y + r.h).concat([1])) + readonly property real k: Math.min((width - 40) / (maxX - minX), (height - 40) / (maxY - minY)) + readonly property real ox: (width - (maxX - minX) * k) / 2 + readonly property real oy: (height - (maxY - minY) * k) / 2 + + Repeater { + model: page.mod.draft.length + Rectangle { + id: mon + required property int index + readonly property var r: canvas.rects[index] ?? { x: 0, y: 0, w: 1, h: 1 } + readonly property var d: page.mod.draft[index] + // The drag offset in canvas pixels, zero when not dragging. + property real dx: 0 + property real dy: 0 + x: canvas.ox + (r.x - canvas.minX) * canvas.k + dx + y: canvas.oy + (r.y - canvas.minY) * canvas.k + dy + width: r.w * canvas.k + height: r.h * canvas.k + radius: 6 + color: Qt.alpha(Theme.accent, drag.pressed ? 0.35 : 0.18) + border.width: 1 + border.color: Qt.alpha(Theme.accent, 0.7) + + Column { + anchors.centerIn: parent + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: mon.d ? mon.d.name : "" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1; bold: true } + color: Theme.text + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: mon.d ? mon.d.model : "" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } + color: Theme.subtext + } + } + + MouseArea { + id: drag + anchors.fill: parent + enabled: !page.mod.pending + cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor + property point start + onPressed: m => start = mapToItem(canvas, m.x, m.y) + onPositionChanged: m => { + const p = mapToItem(canvas, m.x, m.y); + mon.dx = p.x - start.x; + mon.dy = p.y - start.y; + } + onReleased: { + const moved = { x: mon.r.x + mon.dx / canvas.k, y: mon.r.y + mon.dy / canvas.k, w: mon.r.w, h: mon.r.h }; + const others = canvas.rects.filter((o, i) => i !== mon.index); + const s = M.snap(moved, others, 64); + mon.dx = 0; + mon.dy = 0; + if (s.x !== mon.r.x || s.y !== mon.r.y) page.mod.set(mon.index, "position", s.x + "x" + s.y); + } + } + } + } + } + + // ---- cards ---- + // Keyed on orig, a new array on every read, so Discard and a reload + // rebuild the cards and their controls rebind to the draft: Toggle and + // ComboBox set their own checked and currentIndex on a click. + Repeater { + model: page.mod.orig + Rectangle { + id: card + required property int index + readonly property var d: page.mod.draft[index] + readonly property bool auto: !!d && M.AUTO.includes(d.position) + objectName: d ? "mon:" + d.name : "" + width: page.width + height: rows.implicitHeight + 28 + radius: 10 + color: Theme.surface + visible: !!d + + Column { + id: rows + anchors { fill: parent; margins: 14 } + spacing: 8 + + Text { + text: card.d ? card.d.name + " " + card.d.make + " " + card.d.model : "" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1; bold: true } + color: Theme.text + } + + Row { + spacing: 8 + Label { text: "Mode" } + Combo { + width: 220 + model: card.d ? card.d.modes.map(m => m.replace("@", " @ ") + " Hz") : [] + currentIndex: card.d ? card.d.modes.indexOf(card.d.mode) : -1 + onActivated: i => page.mod.set(card.index, "mode", card.d.modes[i]) + } + } + + Row { + spacing: 8 + Label { text: "Scale" } + // Read on every change: a click on Apply does not end editing. + A.Field { + value: card.d ? card.d.scale : "" + onValueChanged: if (card.d && value !== card.d.scale) page.mod.set(card.index, "scale", value) + } + } + + Row { + spacing: 8 + Label { text: "Rotation" } + Repeater { + model: [0, 90, 180, 270] + A.Tab { + required property int modelData + required property int index + text: modelData + "°" + selected: !!card.d && card.d.transform % 4 === index + onClicked: page.mod.set(card.index, "transform", (card.d.transform >= 4 ? 4 : 0) + index) + } + } + A.Toggle { + anchors.verticalCenter: parent.verticalCenter + label: "Flipped" + checked: !!card.d && card.d.transform >= 4 + onToggled: page.mod.set(card.index, "transform", card.d.transform % 4 + (checked ? 4 : 0)) + } + } + + Row { + spacing: 8 + Label { text: "VRR" } + A.Toggle { + anchors.verticalCenter: parent.verticalCenter + label: "Adaptive sync" + checked: !!card.d && card.d.vrr + onToggled: page.mod.set(card.index, "vrr", checked) + } + } + + Row { + spacing: 8 + Label { text: "Position" } + Combo { + width: 220 + model: ["exact"].concat(M.AUTO) + currentIndex: card.auto ? M.AUTO.indexOf(card.d.position) + 1 : 0 + // Rebound after a pick, so a later drag to an exact + // position shows here. + onActivated: i => { + page.mod.set(card.index, "position", i === 0 ? card.d.x + "x" + card.d.y : M.AUTO[i - 1]); + currentIndex = Qt.binding(() => card.auto ? M.AUTO.indexOf(card.d.position) + 1 : 0); + } + } + Text { + anchors.verticalCenter: parent.verticalCenter + visible: !card.auto + text: card.d ? card.d.position.replace("x", ", ") + " drag in the layout to move" : "" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.subtext + } + } + } + } + } + Text { - text: page.mod.draft.map(d => d.name + " " + d.mode).join(", ") - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } - color: Theme.text + width: parent.width + wrapMode: Text.Wrap + text: "Apply tries the settings live. Keep writes sections/monitors.lua; Revert, leaving the page, " + + "or 15 seconds with no answer restores it. Only connected monitors are written." + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.subtext } } |
