Fix QML scope error that disabled all camera movement, and close the lint gap that hid it #3

Merged
branderson merged 4 commits from widget-resolve-linkctl into main 2026-10-07 23:52:29 -07:00
2 changed files with 115 additions and 23 deletions
Showing only changes of commit 29b9971da8 - Show all commits
+18
View File
@@ -43,6 +43,22 @@ else
die "neither kpackagetool6 nor kpackagetool5 found; is this a KDE Plasma session?"
fi
# Qt caches compiled QML and JS on disk. Replacing the applet's files does not
# invalidate that cache reliably, and a stale compiled logic.js missing a
# newer helper function is enough to break bindings that reference it. Clearing
# it costs only a slower first load.
clear_qml_cache() {
local cleared=0 d
for d in "${XDG_CACHE_HOME:-$HOME/.cache}"/plasmashell/qmlcache \
"${XDG_CACHE_HOME:-$HOME/.cache}"/org.kde.plasmashell/qmlcache \
"${XDG_CACHE_HOME:-$HOME/.cache}"/qmlcache; do
if [[ -d $d ]]; then
rm -rf -- "$d" && echo " cleared $d" && cleared=1
fi
done
(( cleared )) || echo " no QML cache to clear"
}
restart_shell() {
(( RESTART )) || return 0
echo "Restarting plasmashell:"
@@ -73,6 +89,8 @@ install)
echo "Installing:"
"$KPT" --type Plasma/Applet --install "$PKG"
fi
echo "Clearing the compiled QML cache:"
clear_qml_cache
restart_shell
echo
echo "Done. Right-click a panel or the desktop, Add Widgets, search 'Insta360'."
+97 -23
View File
@@ -30,11 +30,16 @@ PlasmoidItem {
// `sh -c` with plasmashell's environment, which does not include whatever
// a login shell adds to PATH, so a bare name often cannot be run here
// even when it works in a terminal.
property string resolvedBin: ""
// Starts as the configured name so commands always have something to
// run. The probe only upgrades this to an absolute path; it can never
// leave the widget unable to act.
property string resolvedBin: Plasmoid.configuration.linkctlPath || "linkctl"
property bool resolving: false
// Incremented per command so no two command strings are ever identical.
property int seq: 0
readonly property bool ready: root.resolvedBin !== ""
property string lastCmd: ""
property int lastExit: -1
readonly property int stepMax: Plasmoid.configuration.maxStep
readonly property int tickMs: Plasmoid.configuration.tickInterval
readonly property bool invertPan: Plasmoid.configuration.invertPan
@@ -54,16 +59,28 @@ PlasmoidItem {
property bool keyUp: false
property bool keyDown: false
readonly property var keyVec: Logic.vectorFromKeys({
left: root.keyLeft, right: root.keyRight,
up: root.keyUp, down: root.keyDown
})
readonly property bool keyEngaged: root.keyVec.nx !== 0 || root.keyVec.ny !== 0
// Keyboard vector. Computed defensively on purpose: the pointer path
// reads inNx/inEngaged too, so anything that throws in here would
// disable dragging as well. Qt caches compiled QML and JS on disk, and a
// stale cached logic.js lacking a newer helper is enough to do it.
readonly property bool keyboardAvailable: typeof Logic !== "undefined"
&& typeof Logic.vectorFromKeys === "function"
readonly property var keyVec: root.keyboardAvailable
? Logic.vectorFromKeys({
left: root.keyLeft, right: root.keyRight,
up: root.keyUp, down: root.keyDown
})
: { nx: 0, ny: 0 }
readonly property real keyNx: (root.keyVec && typeof root.keyVec.nx === "number")
? root.keyVec.nx : 0
readonly property real keyNy: (root.keyVec && typeof root.keyVec.ny === "number")
? root.keyVec.ny : 0
readonly property bool keyEngaged: root.keyNx !== 0 || root.keyNy !== 0
// The pointer wins while it is down, so a drag is never fought over by a
// key left held down underneath it.
readonly property real inNx: stick.held ? stick.nx : root.keyVec.nx
readonly property real inNy: stick.held ? stick.ny : root.keyVec.ny
readonly property real inNx: stick.held ? stick.nx : root.keyNx
readonly property real inNy: stick.held ? stick.ny : root.keyNy
readonly property bool inEngaged: stick.engaged || root.keyEngaged
// WASD and the arrow keys. Returns "" for anything that is not a
@@ -114,6 +131,11 @@ PlasmoidItem {
root.refreshTracking();
return;
}
if (typeof Logic.resolveCommand !== "function") {
// Stale cached logic.js: keep the configured name and carry on.
root.refresh();
return;
}
root.statusText = "Looking for linkctl...";
root.resolving = true;
sh.exec(Logic.resolveCommand(root.configuredBin));
@@ -149,6 +171,9 @@ PlasmoidItem {
const err = (data.stderr || "").trim();
disconnectSource(source);
root.busy = false;
root.lastExit = code;
console.log("[insta360] exit", code, "out:", String(out).trim(),
"err:", err);
if (root.resolving) {
root.resolving = false;
@@ -158,8 +183,11 @@ PlasmoidItem {
root.refresh();
root.refreshTracking();
} else {
root.statusText = "linkctl not found. Set its full path "
+ "in this widget's settings.";
// Keep the configured name as the fallback rather than
// blocking: on a packaged install it is on PATH anyway.
root.statusText = "Could not locate linkctl; using '"
+ root.configuredBin + "'.";
root.refresh();
}
return;
}
@@ -176,7 +204,12 @@ PlasmoidItem {
root.seq += 1;
// Never reuse a command string: the engine would serve the
// previous run's cached output and not run anything.
connectSource(Logic.uniqueify(cmd, root.seq));
const unique = (typeof Logic.uniqueify === "function")
? Logic.uniqueify(cmd, root.seq)
: cmd + " # " + root.seq;
root.lastCmd = unique;
console.log("[insta360] run:", unique);
connectSource(unique);
}
}
@@ -194,24 +227,14 @@ PlasmoidItem {
}
function refresh() {
if (!root.ready) {
return;
}
sh.exec(root.baseCmd() + " status");
}
function refreshTracking() {
if (!root.ready) {
return;
}
sh.exec(root.baseCmd() + " tracking status");
}
function action(args) {
if (!root.ready) {
root.resolveBinary();
return;
}
sh.exec(root.baseCmd() + " " + args);
}
@@ -219,7 +242,7 @@ PlasmoidItem {
// Re-resolve if the configured path is edited.
onConfiguredBinChanged: {
root.resolvedBin = "";
root.resolvedBin = root.configuredBin;
root.resolveBinary();
}
@@ -311,11 +334,12 @@ PlasmoidItem {
}
Timer {
id: moveTimer
// Drives both input methods. Only runs while something is asking
// for movement, so an idle popup costs nothing.
interval: root.tickMs
repeat: true
running: root.inEngaged && root.ready
running: root.inEngaged
triggeredOnStart: true
onTriggered: {
if (root.busy) {
@@ -425,6 +449,56 @@ PlasmoidItem {
}
}
PlasmaComponents.Button {
id: diagToggle
Layout.fillWidth: true
focusPolicy: Qt.NoFocus
checkable: true
icon.name: "help-about"
text: "Diagnostics"
}
// Live internal state. Exists because every failure so far has
// presented as "nothing happens", which is the one symptom that
// carries no information.
ColumnLayout {
Layout.fillWidth: true
visible: diagToggle.checked
spacing: 0
Repeater {
model: [
"binary: " + root.resolvedBin,
"configured: " + root.configuredBin,
"device: " + (root.device === "" ? "(auto)" : root.device),
"helpers: vectorFromKeys=" + root.keyboardAvailable
+ " uniqueify="
+ (typeof Logic.uniqueify === "function")
+ " resolveCommand="
+ (typeof Logic.resolveCommand === "function"),
"stick: held=" + stick.held
+ " nx=" + stick.nx.toFixed(2)
+ " ny=" + stick.ny.toFixed(2)
+ " engaged=" + stick.engaged,
"keys: nx=" + root.keyNx.toFixed(2)
+ " ny=" + root.keyNy.toFixed(2)
+ " focus=" + panel.activeFocus,
"input: engaged=" + root.inEngaged
+ " timer=" + moveTimer.running,
"busy: " + root.busy + " seq: " + root.seq
+ " lastExit: " + root.lastExit,
"lastCmd: " + root.lastCmd
]
PlasmaComponents.Label {
Layout.fillWidth: true
text: modelData
font: Kirigami.Theme.smallFont
opacity: 0.8
wrapMode: Text.WrapAnywhere
}
}
}
Item {
Layout.fillHeight: true
}