// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // 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 { 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 } }