mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 02:00:06 +02:00
Merge branch 'layouts-headpin' into experimental
# Conflicts: # README.md # display-settings/ft_display_settings.py # display-settings/main.qml # docs/reference.md
This commit is contained in:
commit
ed9542d3b8
7 files changed
+400
-63
No files matched your search
@@ -10,10 +10,12 @@ the dev container:
|
||||
1920x1080 worth of pixels, rotation for portrait.)
|
||||
- Visibility (ft-screens): when the screens show (always, only with the SteamVR
|
||||
dashboard open, while you look at a controller, or only when toggled), the wrist
|
||||
angle within which a pinned screen shows, and pin or unpin all screens.
|
||||
- Layout: a preset (curved or flat, rows, distance, gap, height) or the arrangement
|
||||
captured from where the screens are now, with a preview; arrange now; save the
|
||||
current arrangement; arrange automatically when the desktop starts.
|
||||
angle within which a pinned screen shows, and pinning each screen to a wrist or
|
||||
your head.
|
||||
- Layout: a preset (curved or flat, rows, distance, gap, height) or a named layout
|
||||
saved from where the screens are, with a preview; arrange now; save the current
|
||||
arrangement under a name; rename and delete; arrange automatically when the
|
||||
desktop starts.
|
||||
- Power: how long the headset can go unused before ft-powerd turns its displays off
|
||||
(DISPLAY_OFF_MIN; the service's state comes from its control socket, @ft_powerd),
|
||||
and whether the Frame stays awake while plugged in, which is Steam's own setting
|
||||
@@ -115,6 +117,7 @@ class Backend(QObject):
|
||||
self._steam_error = ""
|
||||
self._steam_busy = False
|
||||
self._steamDone.connect(self._steam_done, Qt.QueuedConnection)
|
||||
self._pins = [] # each running screen's pin: none | left | right | head
|
||||
self.poll = QTimer(interval=3000, timeout=self._check_running)
|
||||
self.poll.start()
|
||||
self._check_running()
|
||||
@@ -135,13 +138,24 @@ class Backend(QObject):
|
||||
# from the container, so ft_layout.nested_env() doesn't work here).
|
||||
running = os.path.exists(f"/run/user/{os.getuid()}/frametop/wayland-0")
|
||||
count = self._screens_running() if running and ft_layout.backend() == "screens" else 0
|
||||
if running != self._running or count != self._running_count:
|
||||
pins = self._read_pins(count)
|
||||
if running != self._running or count != self._running_count or pins != self._pins:
|
||||
if running != self._running or count != self._running_count:
|
||||
self._started = self._conf() if running else {}
|
||||
self._running = running
|
||||
self._running_count = count
|
||||
self._started = self._conf() if running else {}
|
||||
self._pins = pins
|
||||
self.changed.emit()
|
||||
self._check_powerd()
|
||||
|
||||
def _read_pins(self, count):
|
||||
pins = []
|
||||
for i in range(count):
|
||||
reply = self._ask_screens(f"get {i + 1}")
|
||||
f = reply.split() if reply and reply.startswith("ok") else []
|
||||
pins.append(f[16] if len(f) > 16 else "none")
|
||||
return pins
|
||||
|
||||
def _ask_screens(self, text):
|
||||
"""Request/reply to ft-screens; None if it isn't running."""
|
||||
try:
|
||||
@@ -391,20 +405,23 @@ class Backend(QObject):
|
||||
else:
|
||||
self._ask_screens(f"gesture {v['gesture_hand']} {float(v['gesture_angle']):.1f}")
|
||||
|
||||
@Slot(str)
|
||||
def pinAll(self, hand):
|
||||
reply = self._ask_screens(f"pin all {hand}") if self._running else None
|
||||
if reply and reply.startswith("ok"):
|
||||
self.message.emit(f"All screens ride on your {hand} wrist now; grab a screen's bar to take it off. "
|
||||
"Save current arrangement keeps it.", False)
|
||||
else:
|
||||
self.message.emit(f"Couldn't pin: {reply or 'the desktop is not running'}", True)
|
||||
@Property("QVariantList", notify=changed)
|
||||
def pins(self):
|
||||
return self._pins
|
||||
|
||||
@Slot()
|
||||
def unpinAll(self):
|
||||
reply = self._ask_screens("unpin all") if self._running else None
|
||||
@Slot(str, str)
|
||||
def pin(self, which, where):
|
||||
"""Pin screen `which` (1-based, or "all") to "left", "right", or "head" as it is
|
||||
now, or take it off ("none")."""
|
||||
cmd = f"unpin {which}" if where == "none" else f"pin {which} {where}"
|
||||
reply = self._ask_screens(cmd) if self._running else None
|
||||
if not (reply and reply.startswith("ok")):
|
||||
self.message.emit(f"Couldn't unpin: {reply or 'the desktop is not running'}", True)
|
||||
self.message.emit(f"Couldn't {'unpin' if where == 'none' else 'pin'}: "
|
||||
f"{reply or 'the desktop is not running'}", True)
|
||||
elif which == "all" and where != "none":
|
||||
place = "on your head" if where == "head" else f"on your {where} wrist"
|
||||
self.message.emit(f"All screens ride {place} now. Save current arrangement (Layout) keeps it.", False)
|
||||
self._check_running()
|
||||
|
||||
# --- power: ft-powerd and Steam's sleep setting ---
|
||||
def _check_powerd(self):
|
||||
@@ -513,7 +530,44 @@ class Backend(QObject):
|
||||
|
||||
@Slot(str)
|
||||
def setMode(self, mode):
|
||||
self._edit_layout(lambda l: l.__setitem__("mode", mode))
|
||||
def edit(layout):
|
||||
layout["mode"] = mode
|
||||
layout.pop("active", None)
|
||||
self._edit_layout(edit)
|
||||
|
||||
@Property("QVariantList", notify=changed)
|
||||
def layoutNames(self):
|
||||
return ft_layout.layout_names(ft_layout.load_layout())
|
||||
|
||||
@Slot(str)
|
||||
def useLayout(self, name):
|
||||
"""A named layout as the arrangement (Arrange now puts the screens there)."""
|
||||
try:
|
||||
self._edit_layout(lambda l: ft_layout.use_named(l, name))
|
||||
except RuntimeError as e:
|
||||
self.message.emit(str(e), True)
|
||||
|
||||
@Slot(str)
|
||||
def saveLayout(self, name):
|
||||
try:
|
||||
ft_layout.check_name(name)
|
||||
except RuntimeError as e:
|
||||
return self.message.emit(str(e), True)
|
||||
self._run(f"Saving the arrangement as {' '.join(name.split())}", "save", name)
|
||||
|
||||
@Slot(str, str)
|
||||
def renameLayout(self, old, new):
|
||||
try:
|
||||
self._edit_layout(lambda l: ft_layout.rename_named(l, old, new))
|
||||
except RuntimeError as e:
|
||||
self.message.emit(str(e), True)
|
||||
|
||||
@Slot(str)
|
||||
def deleteLayout(self, name):
|
||||
try:
|
||||
self._edit_layout(lambda l: ft_layout.delete_named(l, name))
|
||||
except RuntimeError as e:
|
||||
self.message.emit(str(e), True)
|
||||
|
||||
@Slot(str, "QVariant")
|
||||
def setPreset(self, key, value):
|
||||
|
||||
+165
-24
@@ -14,7 +14,7 @@ Kirigami.ApplicationWindow {
|
||||
readonly property var pages: backend.backend === "screens"
|
||||
? [{ name: "screens", text: "Screens", icon: "video-display", page: screensPage },
|
||||
{ name: "layout", text: "Layout", icon: "view-grid", page: layoutPage },
|
||||
{ name: "visibility", text: "Visibility & wrist", icon: "view-visible", page: visibilityPage },
|
||||
{ name: "visibility", text: "Visibility & pins", icon: "view-visible", page: visibilityPage },
|
||||
{ name: "power", text: "Power", icon: "preferences-system-power-management", page: powerPage }]
|
||||
: [{ name: "screens", text: "Screens", icon: "video-display", page: screensPage },
|
||||
{ name: "layout", text: "Layout", icon: "view-grid", page: layoutPage },
|
||||
@@ -76,6 +76,89 @@ Kirigami.ApplicationWindow {
|
||||
]
|
||||
}
|
||||
|
||||
// Save the arrangement under a name, or rename a saved layout.
|
||||
Kirigami.PromptDialog {
|
||||
id: nameDialog
|
||||
property string mode: "save" // save | rename
|
||||
property string oldName: ""
|
||||
readonly property var names: backend.layoutNames
|
||||
readonly property string name: nameField.text.trim().split(/\s+/).join(" ")
|
||||
readonly property bool taken: name !== oldName && names.indexOf(name) >= 0
|
||||
readonly property bool ok: name !== "" && !(mode === "rename" && taken)
|
||||
title: mode === "save" ? "Save the arrangement" : "Rename " + oldName
|
||||
standardButtons: Kirigami.Dialog.NoButton
|
||||
|
||||
function openFor(m, text) {
|
||||
mode = m
|
||||
oldName = m === "rename" ? text : ""
|
||||
nameField.text = text
|
||||
open()
|
||||
nameField.forceActiveFocus()
|
||||
nameField.selectAll()
|
||||
}
|
||||
function accept() {
|
||||
if (!ok) return
|
||||
close()
|
||||
if (mode === "save") backend.saveLayout(name)
|
||||
else if (name !== oldName) backend.renameLayout(oldName, name)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
text: nameDialog.mode === "save"
|
||||
? "Where the screens are now, with their sizes, curves, and pins, under this name:"
|
||||
: "New name:"
|
||||
}
|
||||
Controls.TextField {
|
||||
id: nameField
|
||||
Layout.fillWidth: true
|
||||
maximumLength: 40
|
||||
onAccepted: nameDialog.accept()
|
||||
}
|
||||
Controls.Label {
|
||||
visible: nameDialog.taken
|
||||
opacity: 0.7
|
||||
text: nameDialog.mode === "save" ? "Replaces the saved layout with that name."
|
||||
: "There's already a layout with that name."
|
||||
}
|
||||
}
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
text: nameDialog.mode === "save" ? "Save" : "Rename"
|
||||
icon.name: nameDialog.mode === "save" ? "document-save" : "edit-rename"
|
||||
enabled: nameDialog.ok
|
||||
onTriggered: nameDialog.accept()
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: "Cancel"
|
||||
icon.name: "dialog-cancel"
|
||||
onTriggered: nameDialog.close()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: deleteDialog
|
||||
property string name: ""
|
||||
title: "Delete " + name + "?"
|
||||
subtitle: "The screens stay where they are; only the saved layout goes."
|
||||
standardButtons: Kirigami.Dialog.NoButton
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
text: "Delete"
|
||||
icon.name: "edit-delete"
|
||||
onTriggered: { deleteDialog.close(); backend.deleteLayout(deleteDialog.name) }
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: "Cancel"
|
||||
icon.name: "dialog-cancel"
|
||||
onTriggered: deleteDialog.close()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Screens
|
||||
Component {
|
||||
id: screensPage
|
||||
@@ -345,6 +428,15 @@ Kirigami.ApplicationWindow {
|
||||
property var layout: backend.layout
|
||||
property var preset: layout.preset || {}
|
||||
property bool hasCustom: (layout.screens || []).some(s => s.pos !== undefined)
|
||||
// Named layouts: the arrangement is one of them (named) when it came from it, and
|
||||
// hasn't been placed by hand and saved without a name since.
|
||||
property var names: backend.layoutNames
|
||||
property bool fromNamed: names.indexOf(layout.active) >= 0
|
||||
property bool named: layout.mode === "custom" && fromNamed
|
||||
property bool unnamed: names.length === 0 || ((hasCustom || layout.mode === "custom") && !fromNamed)
|
||||
property var choices: [{ text: "Curved around you", value: "arc" }, { text: "Flat wall", value: "flat" }]
|
||||
.concat(names.map(n => ({ text: n, value: "layout:" + n })))
|
||||
.concat(unnamed ? [{ text: names.length ? "Unnamed arrangement" : "Saved arrangement", value: "custom" }] : [])
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
@@ -355,11 +447,12 @@ Kirigami.ApplicationWindow {
|
||||
onTriggered: backend.arrange()
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: "Save current arrangement"
|
||||
text: "Save current arrangement…"
|
||||
icon.name: "document-save"
|
||||
tooltip: "Use where the screens are now (placed by hand) as the layout"
|
||||
tooltip: "Save where the screens are now (placed by hand) as a named layout, and use it"
|
||||
enabled: backend.desktopRunning && backend.busy === ""
|
||||
onTriggered: backend.capture()
|
||||
onTriggered: nameDialog.openFor("save", lpage.named ? lpage.layout.active
|
||||
: "Layout " + (lpage.names.length + 1))
|
||||
}
|
||||
]
|
||||
|
||||
@@ -376,27 +469,49 @@ Kirigami.ApplicationWindow {
|
||||
Kirigami.FormLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Controls.ComboBox {
|
||||
RowLayout {
|
||||
Kirigami.FormData.label: "Arrangement:"
|
||||
model: [
|
||||
{ text: "Curved around you", value: "arc" },
|
||||
{ text: "Flat wall", value: "flat" },
|
||||
{ text: "Saved arrangement", value: "custom" }
|
||||
]
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
currentIndex: lpage.layout.mode === "custom" ? 2 : (lpage.preset.kind === "flat" ? 1 : 0)
|
||||
onActivated: {
|
||||
if (currentValue === "custom") backend.setMode("custom")
|
||||
else backend.setPreset("kind", currentValue)
|
||||
Controls.ComboBox {
|
||||
model: lpage.choices
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
currentIndex: lpage.layout.mode !== "custom" ? (lpage.preset.kind === "flat" ? 1 : 0)
|
||||
: lpage.named ? 2 + lpage.names.indexOf(lpage.layout.active)
|
||||
: lpage.choices.length - 1
|
||||
onActivated: {
|
||||
if (currentValue === "custom") backend.setMode("custom")
|
||||
else if (currentValue.startsWith("layout:")) backend.useLayout(currentValue.slice(7))
|
||||
else backend.setPreset("kind", currentValue)
|
||||
}
|
||||
}
|
||||
Controls.ToolButton {
|
||||
visible: lpage.named
|
||||
icon.name: "edit-rename"
|
||||
text: "Rename…"
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Controls.ToolTip.text: text
|
||||
Controls.ToolTip.visible: hovered
|
||||
onClicked: nameDialog.openFor("rename", lpage.layout.active)
|
||||
}
|
||||
Controls.ToolButton {
|
||||
visible: lpage.named
|
||||
icon.name: "edit-delete"
|
||||
text: "Delete…"
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Controls.ToolTip.text: text
|
||||
Controls.ToolTip.visible: hovered
|
||||
onClicked: { deleteDialog.name = lpage.layout.active; deleteDialog.open() }
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Label {
|
||||
visible: lpage.layout.mode === "custom"
|
||||
Kirigami.FormData.label: ""
|
||||
text: lpage.hasCustom ? "Where the screens were when you saved. Pick a preset to edit."
|
||||
: "Nothing saved yet: place the screens by hand, then Save current arrangement."
|
||||
text: lpage.named ? "Where the screens were when you saved it. Arrange now puts them there. "
|
||||
+ "Save current arrangement updates it or saves a new one."
|
||||
: lpage.hasCustom ? "Where the screens were when you saved. Save current arrangement "
|
||||
+ "names it. Pick a preset to edit."
|
||||
: "Nothing saved yet: place the screens by hand, then Save current arrangement."
|
||||
opacity: 0.7
|
||||
wrapMode: Text.Wrap
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 20
|
||||
@@ -687,10 +802,28 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator { Kirigami.FormData.isSection: true; Kirigami.FormData.label: "Screens on a wrist" }
|
||||
Kirigami.Separator { Kirigami.FormData.isSection: true; Kirigami.FormData.label: "Pinned screens" }
|
||||
|
||||
Repeater {
|
||||
model: backend.pins
|
||||
delegate: Controls.ComboBox {
|
||||
required property var modelData
|
||||
required property int index
|
||||
Kirigami.FormData.label: "Screen " + (index + 1) + ":"
|
||||
model: [
|
||||
{ text: "In the room", value: "none" },
|
||||
{ text: "On the left wrist", value: "left" },
|
||||
{ text: "On the right wrist", value: "right" },
|
||||
{ text: "On your head", value: "head" }
|
||||
]
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
currentIndex: Math.max(0, ["none", "left", "right", "head"].indexOf(modelData))
|
||||
onActivated: backend.pin(String(index + 1), currentValue)
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Kirigami.FormData.label: "Show while facing you within:"
|
||||
Kirigami.FormData.label: "Wrist screens show within:"
|
||||
Controls.Slider {
|
||||
id: wrist
|
||||
from: 20; to: 120; stepSize: 1
|
||||
@@ -705,17 +838,22 @@ Kirigami.ApplicationWindow {
|
||||
Controls.Button {
|
||||
text: "Pin to left wrist"
|
||||
enabled: backend.desktopRunning
|
||||
onClicked: backend.pinAll("left")
|
||||
onClicked: backend.pin("all", "left")
|
||||
}
|
||||
Controls.Button {
|
||||
text: "Pin to right wrist"
|
||||
enabled: backend.desktopRunning
|
||||
onClicked: backend.pinAll("right")
|
||||
onClicked: backend.pin("all", "right")
|
||||
}
|
||||
Controls.Button {
|
||||
text: "Pin to head"
|
||||
enabled: backend.desktopRunning
|
||||
onClicked: backend.pin("all", "head")
|
||||
}
|
||||
Controls.Button {
|
||||
text: "Unpin"
|
||||
enabled: backend.desktopRunning
|
||||
onClicked: backend.unpinAll()
|
||||
onClicked: backend.pin("all", "none")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,7 +868,10 @@ Kirigami.ApplicationWindow {
|
||||
+ "then let go: it rides on that wrist at that size and distance, however far away. To adjust a "
|
||||
+ "pinned screen, grab its bar, move it, and let go (it stays pinned); sweep across the ring to "
|
||||
+ "take it off. It shows while you see its front within the angle above, and fades out beyond "
|
||||
+ "it. Save current arrangement (Layout) keeps pins."
|
||||
+ "it.\n\nPin a screen to your head: choose On your head above. It rides on the headset where it "
|
||||
+ "is now, like a HUD, and shows whenever the screens do. Grab its bar to move it; it stays on "
|
||||
+ "your head where you let go. Choosing a pin above keeps the screen where it is now, so place "
|
||||
+ "it first. Save current arrangement (Layout) keeps pins."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user