gestures: consumer + routing table, wired to the dial
This commit is contained in:
parent
bef6e6a653
commit
002a1a7b08
5 changed files with 151 additions and 0 deletions
|
|
@ -38,6 +38,7 @@ services/Ai.qml souveraine/services/Ai.qml
|
||||||
services/Cellular.qml souveraine/services/Cellular.qml
|
services/Cellular.qml souveraine/services/Cellular.qml
|
||||||
services/ChargeRate.qml souveraine/services/ChargeRate.qml
|
services/ChargeRate.qml souveraine/services/ChargeRate.qml
|
||||||
services/Haptics.qml souveraine/services/Haptics.qml
|
services/Haptics.qml souveraine/services/Haptics.qml
|
||||||
|
services/Gestures.qml souveraine/services/Gestures.qml
|
||||||
modules/souveraine/dial/RadialDial.qml souveraine/modules/souveraine/dial/RadialDial.qml
|
modules/souveraine/dial/RadialDial.qml souveraine/modules/souveraine/dial/RadialDial.qml
|
||||||
modules/souveraine/dial/DialHost.qml souveraine/modules/souveraine/dial/DialHost.qml
|
modules/souveraine/dial/DialHost.qml souveraine/modules/souveraine/dial/DialHost.qml
|
||||||
modules/souveraine/dial/qmldir souveraine/modules/souveraine/dial/qmldir
|
modules/souveraine/dial/qmldir souveraine/modules/souveraine/dial/qmldir
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,8 @@ Scope {
|
||||||
|
|
||||||
function open(): void {
|
function open(): void {
|
||||||
scope.dialOpen = true;
|
scope.dialOpen = true;
|
||||||
|
console.log("[dial] open -> dialOpen=" + scope.dialOpen
|
||||||
|
+ " screens=" + Quickshell.screens.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): void {
|
function close(): void {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
|
||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
|
import qs.services
|
||||||
// AppInventory.qml root is Item (not QtObject) so it can own its scanner
|
// AppInventory.qml root is Item (not QtObject) so it can own its scanner
|
||||||
// Process child — the original QtObject root failed with "no default property."
|
// Process child — the original QtObject root failed with "no default property."
|
||||||
import qs.modules.ii.appInventory
|
import qs.modules.ii.appInventory
|
||||||
|
|
@ -63,6 +64,12 @@ Scope {
|
||||||
PanelLoader { component: Overview {} }
|
PanelLoader { component: Overview {} }
|
||||||
PanelLoader { component: Polkit {} }
|
PanelLoader { component: Polkit {} }
|
||||||
PanelLoader { component: DialHost {} }
|
PanelLoader { component: DialHost {} }
|
||||||
|
|
||||||
|
// Gestures is a singleton and QML instantiates singletons lazily, so its
|
||||||
|
// IpcHandler never registered — `gesture` answered "Target not found"
|
||||||
|
// while the file was plainly deployed. Touching a property is what brings
|
||||||
|
// it into existence. Same reason AppInventoryScope is a plain child.
|
||||||
|
Component.onCompleted: Gestures.squeezeAction
|
||||||
PanelLoader { component: RegionSelector {} }
|
PanelLoader { component: RegionSelector {} }
|
||||||
PanelLoader { component: ScreenCorners {} }
|
PanelLoader { component: ScreenCorners {} }
|
||||||
PanelLoader { component: ScreenTranslator {} }
|
PanelLoader { component: ScreenTranslator {} }
|
||||||
|
|
|
||||||
140
surfaces/quickshell/services/Gestures.qml
Normal file
140
surfaces/quickshell/services/Gestures.qml
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
// Gesture consumer — the userspace side of squeeze, and the seam every other
|
||||||
|
// physical gesture arrives through.
|
||||||
|
//
|
||||||
|
// This is deliberately written before the producer exists. TASK-13 has the
|
||||||
|
// Active Edge rail powered (PM8998 GPIO 2, held from boot) and the SSC
|
||||||
|
// registry admitting `sns_touch_gesture`, but nothing had anywhere to deliver
|
||||||
|
// a squeeze TO — and a bring-up with no consumer is a sensor that fires into
|
||||||
|
// nothing, which is exactly why grip stalled. The contract goes first now, so
|
||||||
|
// the producer has a defined target and can be tested the moment it works.
|
||||||
|
//
|
||||||
|
// The producer is not specified here on purpose. Anything that can reach the
|
||||||
|
// shell's IPC socket can deliver a gesture: a libssc client, a udev-spawned
|
||||||
|
// helper, an evdev reader, or a human running `qs ipc call gesture squeeze`
|
||||||
|
// to test the routing without any of that existing. That is the same posture
|
||||||
|
// the sensor reporters take — the source is replaceable, the contract is not.
|
||||||
|
//
|
||||||
|
// Routing is NOT hardcoded per gesture. `squeezeAction` names an action, and
|
||||||
|
// the action table below is the one place a gesture's meaning is decided, so
|
||||||
|
// a squeeze can be re-pointed without editing call sites. See the Keyboard
|
||||||
|
// System note in TASK-13 step 4: a squeeze must register as an input, not as
|
||||||
|
// a hardcoded binding, or it collides with everything else that wants it.
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs
|
||||||
|
import qs.modules.common
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// What each gesture does. Names, not closures, so the choice is data a
|
||||||
|
// settings page or an agent can read and change.
|
||||||
|
property string squeezeAction: "dial"
|
||||||
|
property string squeezeHoldAction: "assistant"
|
||||||
|
property string edgeAction: "dial"
|
||||||
|
|
||||||
|
// Last gesture seen, so a surface can show that the hardware is alive even
|
||||||
|
// before anything is bound to it — the difference between "grip does
|
||||||
|
// nothing" and "grip is not reaching us" is the whole debugging story.
|
||||||
|
property string lastGesture: ""
|
||||||
|
property double lastGestureAt: 0
|
||||||
|
|
||||||
|
signal gestureReceived(string name)
|
||||||
|
|
||||||
|
readonly property var actions: ({
|
||||||
|
"dial": () => {
|
||||||
|
Haptics.tick();
|
||||||
|
Quickshell.execDetached(["qs", "-c", "souveraine", "ipc",
|
||||||
|
"--any-display", "call", "dial", "toggle"]);
|
||||||
|
},
|
||||||
|
"assistant": () => {
|
||||||
|
Haptics.tick();
|
||||||
|
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
|
||||||
|
},
|
||||||
|
"keyboard": () => {
|
||||||
|
Haptics.tick();
|
||||||
|
GlobalStates.oskOpen = !GlobalStates.oskOpen;
|
||||||
|
},
|
||||||
|
"screenshot": () => {
|
||||||
|
Haptics.confirm();
|
||||||
|
Quickshell.execDetached(["sh", "-c",
|
||||||
|
"grim ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png"]);
|
||||||
|
},
|
||||||
|
"none": () => {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// One entry point for every gesture, so the trail of "what arrived" is in
|
||||||
|
// one place and a refusal is legible rather than a silent no-op.
|
||||||
|
function deliver(name, action) {
|
||||||
|
root.lastGesture = name;
|
||||||
|
root.lastGestureAt = Date.now();
|
||||||
|
console.log("[gesture] " + name + " -> " + action);
|
||||||
|
root.gestureReceived(name);
|
||||||
|
|
||||||
|
const fn = root.actions[action];
|
||||||
|
if (!fn) {
|
||||||
|
console.log("[gesture] no action named '" + action + "'");
|
||||||
|
Haptics.refuse();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fn();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "gesture"
|
||||||
|
|
||||||
|
// The squeeze. Called by whatever ends up producing it.
|
||||||
|
function squeeze(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
ok: root.deliver("squeeze", root.squeezeAction),
|
||||||
|
action: root.squeezeAction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// A held squeeze is a different gesture, not a longer one.
|
||||||
|
function squeezeHold(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
ok: root.deliver("squeeze-hold", root.squeezeHoldAction),
|
||||||
|
action: root.squeezeHoldAction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function edge(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
ok: root.deliver("edge", root.edgeAction),
|
||||||
|
action: root.edgeAction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// What is bound to what, and what was last seen. An agent or a
|
||||||
|
// settings page reads this instead of guessing.
|
||||||
|
function state(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
squeeze: root.squeezeAction,
|
||||||
|
squeezeHold: root.squeezeHoldAction,
|
||||||
|
edge: root.edgeAction,
|
||||||
|
available: Object.keys(root.actions),
|
||||||
|
lastGesture: root.lastGesture,
|
||||||
|
lastGestureAt: root.lastGestureAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-point a gesture without editing code.
|
||||||
|
function bind(gesture: string, action: string): string {
|
||||||
|
if (!root.actions[action])
|
||||||
|
return JSON.stringify({ ok: false, reason: "no such action: " + action });
|
||||||
|
switch (gesture) {
|
||||||
|
case "squeeze": root.squeezeAction = action; break;
|
||||||
|
case "squeeze-hold": root.squeezeHoldAction = action; break;
|
||||||
|
case "edge": root.edgeAction = action; break;
|
||||||
|
default:
|
||||||
|
return JSON.stringify({ ok: false, reason: "no such gesture: " + gesture });
|
||||||
|
}
|
||||||
|
return JSON.stringify({ ok: true, gesture: gesture, action: action });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ singleton EasyEffects 1.0 EasyEffects.qml
|
||||||
singleton Emojis 1.0 Emojis.qml
|
singleton Emojis 1.0 Emojis.qml
|
||||||
singleton FileSearch 1.0 FileSearch.qml
|
singleton FileSearch 1.0 FileSearch.qml
|
||||||
singleton FirstRunExperience 1.0 FirstRunExperience.qml
|
singleton FirstRunExperience 1.0 FirstRunExperience.qml
|
||||||
|
singleton Gestures 1.0 Gestures.qml
|
||||||
singleton GlobalFocusGrab 1.0 GlobalFocusGrab.qml
|
singleton GlobalFocusGrab 1.0 GlobalFocusGrab.qml
|
||||||
singleton GoogleCloud 1.0 GoogleCloud.qml
|
singleton GoogleCloud 1.0 GoogleCloud.qml
|
||||||
singleton Haptics 1.0 Haptics.qml
|
singleton Haptics 1.0 Haptics.qml
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue