diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-30 20:40:07 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-30 20:40:07 +0100 |
| commit | a5d7986658a67803cbd204ac6e7ebc39b00e853c (patch) | |
| tree | 0da7d825e97ffd84b84db6d19abb6b4ee73273e6 /desktop/modules/monitors/MonitorsPage.qml | |
| parent | 7543de352143e1d09df5ba3e265311d52a60840d (diff) | |
| download | quickshell-a5d7986658a67803cbd204ac6e7ebc39b00e853c.tar.gz quickshell-a5d7986658a67803cbd204ac6e7ebc39b00e853c.zip | |
monitors: the layout canvas and the per-monitor cards
Monitors drawn at logical size, fitted to the canvas; a drag moves one by
an offset and on release snaps it within 64 logical px of the other's
edges and writes an exact position. The cards set mode from the
deduplicated list, scale (read on every change, since a click on Apply
does not end editing), rotation and flip, VRR, and auto or exact
position. Leaving the page with an Apply unanswered reverts it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/monitors/MonitorsPage.qml')
| -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 } } |
