311 lines
12 KiB
QML
311 lines
12 KiB
QML
import QtQuick
|
||
import qs.config
|
||
import qs.widgets
|
||
import "../../services/DisplayLayout.js" as DisplayLayout
|
||
|
||
Item {
|
||
id: root
|
||
|
||
required property var displayService
|
||
property string selectedOutput: ""
|
||
property var draftLayout: []
|
||
property bool interactionEnabled: true
|
||
|
||
signal selectionRequested(string output)
|
||
|
||
implicitHeight: content.implicitHeight
|
||
readonly property var canvasData: DisplayLayout.canvasRects(
|
||
root.draftLayout, canvas.width, canvas.height, 18)
|
||
|
||
function copied(layout): var {
|
||
return (layout || []).map(record => Object.assign({}, record));
|
||
}
|
||
|
||
function resetDraft(): void {
|
||
root.draftLayout = root.copied(root.displayService.currentLayout());
|
||
}
|
||
|
||
function setDraftPosition(output: string, x: real, y: real, snapToEdges: bool): bool {
|
||
const next = root.copied(root.draftLayout);
|
||
const record = next.find(candidate => candidate.name === output);
|
||
if (!record)
|
||
return false;
|
||
record.x = Math.round(x);
|
||
record.y = Math.round(y);
|
||
root.draftLayout = snapToEdges ? DisplayLayout.snap(next, output, 16) : next;
|
||
return true;
|
||
}
|
||
|
||
function nudge(output: string, dx: int, dy: int): bool {
|
||
const record = root.draftLayout.find(candidate => candidate.name === output);
|
||
return !!record && root.setDraftPosition(output, record.x + dx, record.y + dy, false);
|
||
}
|
||
|
||
function applyDraft(): bool {
|
||
return root.displayService.applyLayout(root.copied(root.draftLayout));
|
||
}
|
||
|
||
function makePrimary(output: string): bool {
|
||
const next = root.copied(root.draftLayout);
|
||
if (!next.some(record => record.name === output))
|
||
return false;
|
||
for (const record of next)
|
||
record.primary = record.name === output;
|
||
root.draftLayout = DisplayLayout.normalize(next);
|
||
return root.applyDraft();
|
||
}
|
||
|
||
function canvasSnapshot(): var {
|
||
return {
|
||
bounds: root.canvasData.bounds,
|
||
scale: root.canvasData.scale,
|
||
rects: root.canvasData.rects.map(record => Object.assign({}, record))
|
||
};
|
||
}
|
||
|
||
Component.onCompleted: root.resetDraft()
|
||
|
||
Connections {
|
||
target: root.displayService
|
||
ignoreUnknownSignals: true
|
||
function onMonitorsChanged(): void {
|
||
if (!root.displayService.awaitingConfirmation)
|
||
root.resetDraft();
|
||
}
|
||
}
|
||
|
||
Column {
|
||
id: content
|
||
width: parent.width
|
||
spacing: 11
|
||
|
||
Rectangle {
|
||
id: canvas
|
||
width: parent.width
|
||
height: root.width >= 620 ? 232 : 190
|
||
radius: Theme.cardRadius
|
||
color: Theme.alpha(Theme.bgDark, 0.76)
|
||
border.width: 1
|
||
border.color: Theme.alpha(Theme.fg, 0.07)
|
||
clip: true
|
||
|
||
// A restrained coordinate field makes the topology feel like a
|
||
// precision instrument without turning it into a technical graph.
|
||
Repeater {
|
||
model: 4
|
||
Rectangle {
|
||
required property int index
|
||
y: (index + 1) * canvas.height / 5
|
||
width: canvas.width
|
||
height: 1
|
||
color: Theme.alpha(Theme.fg, 0.025)
|
||
}
|
||
}
|
||
|
||
Repeater {
|
||
model: root.canvasData.rects
|
||
|
||
Rectangle {
|
||
id: tile
|
||
|
||
required property var modelData
|
||
readonly property bool selected: root.selectedOutput === modelData.name
|
||
readonly property var draft: root.draftLayout.find(
|
||
record => record.name === modelData.name)
|
||
|
||
x: modelData.x
|
||
y: modelData.y
|
||
width: Math.max(64, modelData.width)
|
||
height: Math.max(48, modelData.height)
|
||
radius: 11
|
||
color: tile.selected
|
||
? Theme.alpha(Theme.bgHighlight, 0.92)
|
||
: Theme.alpha(Theme.bgPanel, hover.hovered ? 0.94 : 0.78)
|
||
border.width: tile.selected || activeFocus ? 2 : 1
|
||
border.color: activeFocus
|
||
? Theme.accentSecondary
|
||
: (tile.selected ? Theme.accent : Theme.alpha(Theme.fg, 0.14))
|
||
opacity: root.interactionEnabled ? 1 : 0.5
|
||
activeFocusOnTab: root.interactionEnabled
|
||
|
||
Accessible.role: Accessible.Button
|
||
Accessible.name: "Move " + tile.modelData.name
|
||
Accessible.description: tile.draft && tile.draft.primary
|
||
? "Primary display. Drag or use the arrow keys to move it."
|
||
: "Drag or use the arrow keys to move this display."
|
||
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
anchors.margins: 2
|
||
radius: parent.radius - 2
|
||
visible: tile.selected
|
||
opacity: 0.24
|
||
gradient: Gradient {
|
||
orientation: Gradient.Horizontal
|
||
GradientStop { position: 0; color: Theme.accent }
|
||
GradientStop { position: 1; color: Theme.accentSecondary }
|
||
}
|
||
}
|
||
|
||
Column {
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
anchors.margins: 11
|
||
spacing: 2
|
||
|
||
Text {
|
||
width: parent.width
|
||
text: tile.modelData.name
|
||
color: Theme.fg
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSize
|
||
font.weight: Font.DemiBold
|
||
elide: Text.ElideRight
|
||
}
|
||
Text {
|
||
width: parent.width
|
||
text: tile.draft
|
||
? `${Math.round(tile.draft.width / tile.draft.scale)} × ${Math.round(tile.draft.height / tile.draft.scale)}`
|
||
: ""
|
||
color: Theme.fgDim
|
||
font.family: Theme.fontFamily
|
||
font.features: Theme.tabularFigures
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
elide: Text.ElideRight
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
anchors.top: parent.top
|
||
anchors.right: parent.right
|
||
anchors.margins: 7
|
||
width: primaryText.implicitWidth + 12
|
||
height: 20
|
||
radius: Theme.pillRadius
|
||
visible: tile.draft && tile.draft.primary
|
||
color: Theme.alpha(Theme.accent, 0.2)
|
||
|
||
Text {
|
||
id: primaryText
|
||
anchors.centerIn: parent
|
||
text: "Primary"
|
||
color: Theme.accentAlt
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
|
||
font.weight: Font.DemiBold
|
||
}
|
||
}
|
||
|
||
HoverHandler {
|
||
id: hover
|
||
enabled: root.interactionEnabled
|
||
cursorShape: Qt.OpenHandCursor
|
||
}
|
||
|
||
TapHandler {
|
||
enabled: root.interactionEnabled
|
||
onTapped: {
|
||
root.selectionRequested(tile.modelData.name);
|
||
tile.forceActiveFocus();
|
||
}
|
||
}
|
||
|
||
DragHandler {
|
||
id: drag
|
||
target: null
|
||
enabled: root.interactionEnabled
|
||
property real initialX: 0
|
||
property real initialY: 0
|
||
property bool moved: false
|
||
|
||
onActiveChanged: {
|
||
if (active) {
|
||
const record = root.draftLayout.find(
|
||
candidate => candidate.name === tile.modelData.name);
|
||
initialX = record ? record.x : 0;
|
||
initialY = record ? record.y : 0;
|
||
moved = false;
|
||
root.selectionRequested(tile.modelData.name);
|
||
tile.forceActiveFocus();
|
||
} else if (moved) {
|
||
const record = root.draftLayout.find(
|
||
candidate => candidate.name === tile.modelData.name);
|
||
if (record) {
|
||
root.setDraftPosition(tile.modelData.name, record.x, record.y, true);
|
||
root.applyDraft();
|
||
}
|
||
}
|
||
}
|
||
onTranslationChanged: {
|
||
if (!active || root.canvasData.scale <= 0)
|
||
return;
|
||
moved = true;
|
||
root.setDraftPosition(
|
||
tile.modelData.name,
|
||
initialX + translation.x / root.canvasData.scale,
|
||
initialY + translation.y / root.canvasData.scale,
|
||
false);
|
||
}
|
||
}
|
||
|
||
Keys.onPressed: event => {
|
||
if (!root.interactionEnabled)
|
||
return;
|
||
const step = event.modifiers & Qt.ShiftModifier ? 100 : 10;
|
||
let handled = true;
|
||
if (event.key === Qt.Key_Left)
|
||
root.nudge(tile.modelData.name, -step, 0);
|
||
else if (event.key === Qt.Key_Right)
|
||
root.nudge(tile.modelData.name, step, 0);
|
||
else if (event.key === Qt.Key_Up)
|
||
root.nudge(tile.modelData.name, 0, -step);
|
||
else if (event.key === Qt.Key_Down)
|
||
root.nudge(tile.modelData.name, 0, step);
|
||
else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter)
|
||
root.applyDraft();
|
||
else if (event.key === Qt.Key_Escape)
|
||
root.resetDraft();
|
||
else
|
||
handled = false;
|
||
event.accepted = handled;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Row {
|
||
width: parent.width
|
||
spacing: 8
|
||
|
||
Text {
|
||
width: Math.max(0, parent.width - primaryButton.width - applyButton.width - 16)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: root.width >= 600
|
||
? "Drag to arrange · arrows move 10 px · Shift moves 100 px"
|
||
: "Drag or use the arrow keys"
|
||
color: Theme.fgMuted
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
SettingsButton {
|
||
id: primaryButton
|
||
text: "Make primary"
|
||
enabled: root.interactionEnabled && root.selectedOutput !== ""
|
||
&& !root.draftLayout.find(record =>
|
||
record.name === root.selectedOutput)?.primary
|
||
onClicked: root.makePrimary(root.selectedOutput)
|
||
}
|
||
|
||
SettingsButton {
|
||
id: applyButton
|
||
text: "Apply"
|
||
enabled: root.interactionEnabled
|
||
onClicked: root.applyDraft()
|
||
}
|
||
}
|
||
}
|
||
}
|