213 lines
5.4 KiB
QML
213 lines
5.4 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Wayland
|
|
import Quickshell.Io
|
|
|
|
// monitor layout picker for $DOTWRYN/config/desktop/hyprland/config.lua.d/02-monitor-layouts.lua;
|
|
// shown with 'qs ipc call -- displays show' (without '--', 'show' parses as 'qs ipc show')
|
|
Scope {
|
|
id: picker
|
|
|
|
property bool shown: false
|
|
property var screen: null
|
|
|
|
property var layouts: []
|
|
property var resolutions: []
|
|
|
|
property int selectedLayout: 0
|
|
property int selectedResolution: -1 // -1 : the modes configured in the layout
|
|
|
|
function show(): void {
|
|
selectedLayout = 0
|
|
selectedResolution = -1
|
|
screen = Quickshell.screens.find(screen => screen.name === Hyprland.focusedMonitor?.name) ?? null
|
|
Quickshell.execDetached(["hyprctl", "eval", "dotwryn_publish_monitor_layouts()"])
|
|
shown = true
|
|
}
|
|
|
|
function moveLayout(offset: int): void {
|
|
if (layouts.length > 0) selectedLayout = (selectedLayout + offset + layouts.length) % layouts.length
|
|
}
|
|
|
|
function moveResolution(offset: int): void {
|
|
const count = resolutions.length + 1
|
|
selectedResolution = ((selectedResolution + 1 + offset + count) % count) - 1
|
|
}
|
|
|
|
// hyprland 0.56.2 segfaults if a focused layer surface is destroyed by a
|
|
// monitor change, so the picker must be gone before the layout is applied
|
|
function applyLayout(): void {
|
|
if (selectedLayout >= layouts.length) return
|
|
|
|
const resolution = selectedResolution < 0 ? "nil" : JSON.stringify(resolutions[selectedResolution])
|
|
evaluation.command = ["hyprctl", "eval", `dotwryn_set_monitor_layout(${JSON.stringify(layouts[selectedLayout])}, ${resolution})`]
|
|
|
|
shown = false
|
|
applyAfterHidden.restart()
|
|
}
|
|
|
|
Timer {
|
|
id: applyAfterHidden
|
|
interval: 250
|
|
onTriggered: evaluation.running = true
|
|
}
|
|
|
|
Process {
|
|
id: evaluation
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const result = text.trim()
|
|
if (result !== "ok") Quickshell.execDetached(["notify-send", "displays", result.replace(/^.*:\d+: /, "")])
|
|
}
|
|
}
|
|
}
|
|
|
|
IpcHandler {
|
|
target: "displays"
|
|
|
|
function show(): void { picker.show() }
|
|
function hide(): void { picker.shown = false }
|
|
}
|
|
|
|
Connections {
|
|
target: Hyprland
|
|
|
|
function onRawEvent(event) {
|
|
const prefix = "dotwryn-monitor-layouts "
|
|
if (event.name !== "custom" || !event.data.startsWith(prefix)) return
|
|
|
|
const published = JSON.parse(event.data.slice(prefix.length))
|
|
picker.layouts = published.layouts
|
|
picker.resolutions = published.resolutions
|
|
}
|
|
}
|
|
|
|
component PickerText: Text {
|
|
color: Colors.foreground
|
|
font.family: Colors.font
|
|
font.pointSize: Colors.fontSize
|
|
}
|
|
|
|
PanelWindow {
|
|
visible: picker.shown
|
|
screen: picker.screen
|
|
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
implicitWidth: content.implicitWidth + 24
|
|
implicitHeight: content.implicitHeight + 24
|
|
color: "transparent"
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Colors.background
|
|
border.width: 1
|
|
border.color: Colors.primary
|
|
|
|
focus: true
|
|
Keys.onPressed: event => {
|
|
switch (event.key) {
|
|
case Qt.Key_J: case Qt.Key_Down: picker.moveLayout(1); break
|
|
case Qt.Key_K: case Qt.Key_Up: picker.moveLayout(-1); break
|
|
case Qt.Key_L: case Qt.Key_Right: picker.moveResolution(1); break
|
|
case Qt.Key_H: case Qt.Key_Left: picker.moveResolution(-1); break
|
|
|
|
case Qt.Key_Return: case Qt.Key_Enter: picker.applyLayout(); break
|
|
|
|
case Qt.Key_Escape: case Qt.Key_Q: picker.shown = false; break
|
|
default: return
|
|
}
|
|
event.accepted = true
|
|
}
|
|
|
|
Column {
|
|
id: content
|
|
anchors.centerIn: parent
|
|
spacing: 4
|
|
|
|
PickerText {
|
|
text: "monitor layout"
|
|
font.bold: true
|
|
color: Colors.secondary
|
|
}
|
|
|
|
PickerText {
|
|
visible: picker.layouts.length === 0
|
|
text: "no monitor layouts for this host"
|
|
color: Colors.disabled
|
|
}
|
|
|
|
Repeater {
|
|
model: picker.layouts
|
|
|
|
Rectangle {
|
|
id: layoutRow
|
|
|
|
required property int index
|
|
required property string modelData
|
|
|
|
readonly property bool selected: picker.selectedLayout === index
|
|
|
|
implicitWidth: Math.max(layoutLabel.implicitWidth + 24, 280)
|
|
implicitHeight: layoutLabel.implicitHeight + 8
|
|
color: selected ? Colors.primary : "transparent"
|
|
|
|
PickerText {
|
|
id: layoutLabel
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: layoutRow.modelData
|
|
font.bold: layoutRow.selected
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onEntered: picker.selectedLayout = layoutRow.index
|
|
onClicked: picker.applyLayout()
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
visible: picker.layouts.length > 0
|
|
spacing: 8
|
|
topPadding: 8
|
|
|
|
PickerText {
|
|
text: "resolution"
|
|
color: Colors.secondary
|
|
}
|
|
|
|
Repeater {
|
|
model: ["configured"].concat(picker.resolutions)
|
|
|
|
PickerText {
|
|
required property int index
|
|
required property string modelData
|
|
|
|
readonly property bool selected: picker.selectedResolution === index - 1
|
|
|
|
text: selected ? `[${modelData}]` : ` ${modelData} `
|
|
color: selected ? Colors.foreground : Colors.disabled
|
|
font.bold: selected
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: picker.selectedResolution = parent.index - 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|