mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 03:00:06 +02:00
Hands: fix Export doing nothing, and show it's busy at once
af2ea7c put _stay_awake between exportSession and its @Slot, so the
window's Export button called a method QML couldn't see. A new test checks
every backend call in main.qml against Backend's slots and properties.
Export and Upload now say Exporting…/Uploading… with a spinner the moment
they're pressed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af2ea7c0ee
commit
8dbcdecf90
3 files changed
+61
-4
No files matched your search
@@ -787,7 +787,6 @@ class Backend(QObject):
|
|||||||
def exportSessionId(self):
|
def exportSessionId(self):
|
||||||
return self._export_session
|
return self._export_session
|
||||||
|
|
||||||
@Slot(str, bool)
|
|
||||||
def _stay_awake(self, why, on):
|
def _stay_awake(self, why, on):
|
||||||
"""Hold off sleep while an export or upload runs, so the headset can be taken off and
|
"""Hold off sleep while an export or upload runs, so the headset can be taken off and
|
||||||
left plugged in: a host unit running systemd-inhibit, started with the first reason
|
left plugged in: a host unit running systemd-inhibit, started with the first reason
|
||||||
@@ -812,6 +811,7 @@ class Backend(QObject):
|
|||||||
print(f"keeping the Frame awake: {e}", file=sys.stderr, flush=True)
|
print(f"keeping the Frame awake: {e}", file=sys.stderr, flush=True)
|
||||||
self._thread(run)
|
self._thread(run)
|
||||||
|
|
||||||
|
@Slot(str, bool)
|
||||||
def exportSession(self, session, keep_notes):
|
def exportSession(self, session, keep_notes):
|
||||||
if self.exporting or self._active_guard(session):
|
if self.exporting or self._active_guard(session):
|
||||||
return
|
return
|
||||||
|
|||||||
+17
-3
@@ -1396,12 +1396,20 @@ Kirigami.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
text: exportView.chosen && exportView.chosen.exported ? "Export again" : "Export"
|
text: backend.exporting ? "Exporting…"
|
||||||
|
: exportView.chosen && exportView.chosen.exported ? "Export again" : "Export"
|
||||||
icon.name: "document-export"
|
icon.name: "document-export"
|
||||||
enabled: exportView.chosen !== null && !backend.exporting && backend.zstdFound
|
enabled: exportView.chosen !== null && !backend.exporting && backend.zstdFound
|
||||||
&& !exportView.chosen.active
|
&& !exportView.chosen.active
|
||||||
onClicked: backend.exportSession(exportView.chosen.id, false)
|
onClicked: backend.exportSession(exportView.chosen.id, false)
|
||||||
}
|
}
|
||||||
|
// Shows the moment Export is pressed: the first progress can take a few seconds.
|
||||||
|
Controls.BusyIndicator {
|
||||||
|
visible: backend.exporting
|
||||||
|
running: visible
|
||||||
|
implicitWidth: Kirigami.Units.gridUnit * 1.5
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
}
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
visible: backend.exporting
|
visible: backend.exporting
|
||||||
text: "Cancel"
|
text: "Cancel"
|
||||||
@@ -1426,7 +1434,7 @@ Kirigami.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
Controls.Label {
|
Controls.Label {
|
||||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 26
|
Layout.maximumWidth: Kirigami.Units.gridUnit * 26
|
||||||
visible: backend.exportText !== "" && backend.exportSessionId === sessionBox.currentValue
|
visible: backend.exportText !== "" && (backend.exporting || backend.exportSessionId === sessionBox.currentValue)
|
||||||
wrapMode: Text.WrapAnywhere
|
wrapMode: Text.WrapAnywhere
|
||||||
text: backend.exportText
|
text: backend.exportText
|
||||||
}
|
}
|
||||||
@@ -1643,11 +1651,17 @@ Kirigami.ApplicationWindow {
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
visible: uploadView.session !== ""
|
visible: uploadView.session !== ""
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
text: backend.hubDryRun ? "Upload (dry run)" : "Upload"
|
text: backend.uploading ? "Uploading…" : backend.hubDryRun ? "Upload (dry run)" : "Upload"
|
||||||
icon.name: "cloud-upload"
|
icon.name: "cloud-upload"
|
||||||
enabled: uploadView.blocked === "" && !backend.uploading
|
enabled: uploadView.blocked === "" && !backend.uploading
|
||||||
onClicked: uploadView.startUpload()
|
onClicked: uploadView.startUpload()
|
||||||
}
|
}
|
||||||
|
Controls.BusyIndicator {
|
||||||
|
visible: backend.uploading
|
||||||
|
running: visible
|
||||||
|
implicitWidth: Kirigami.Units.gridUnit * 1.5
|
||||||
|
implicitHeight: implicitWidth
|
||||||
|
}
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
visible: backend.uploading
|
visible: backend.uploading
|
||||||
text: "Cancel"
|
text: "Cancel"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""main.qml against ft_handrec.Backend: every backend.name(...) the window calls is a slot, and
|
||||||
|
every backend.name it reads is a property or a slot. A method that lost its @Slot shows up in
|
||||||
|
QML only as "is not a function" when its button is pressed (2026-10-03: Export did nothing).
|
||||||
|
Needs PySide6 (the dev container); skipped without it.
|
||||||
|
|
||||||
|
python3 hands/rec/tests/test_qml_backend.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
REC = os.path.dirname(HERE)
|
||||||
|
sys.path.insert(0, REC)
|
||||||
|
|
||||||
|
|
||||||
|
class QmlBackendTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
try:
|
||||||
|
import ft_handrec
|
||||||
|
except ImportError as e:
|
||||||
|
self.skipTest(f"no PySide6: {e}")
|
||||||
|
meta = ft_handrec.Backend.staticMetaObject
|
||||||
|
self.slots = {bytes(meta.method(i).name()).decode() for i in range(meta.methodCount())}
|
||||||
|
self.props = {meta.property(i).name() for i in range(meta.propertyCount())}
|
||||||
|
with open(os.path.join(REC, "main.qml")) as f:
|
||||||
|
self.qml = f.read()
|
||||||
|
|
||||||
|
def test_calls_are_slots(self):
|
||||||
|
called = set(re.findall(r"\bbackend\.(\w+)\s*\(", self.qml))
|
||||||
|
self.assertTrue(called)
|
||||||
|
self.assertEqual(sorted(called - self.slots), [], "called from main.qml but not a slot")
|
||||||
|
|
||||||
|
def test_reads_exist(self):
|
||||||
|
read = set(re.findall(r"\bbackend\.(\w+)\b(?!\s*\()", self.qml))
|
||||||
|
self.assertTrue(read)
|
||||||
|
self.assertEqual(sorted(read - self.props - self.slots), [], "read in main.qml but not on Backend")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user