mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
Keyboard clicks at the gaze: Meta+J and Meta+K
gaze_left and gaze_right (Meta+J and Meta+K by default) click where you look.
A tap clicks where the dot was at the press and tells the gaze tracker it was
right there. Held, the pointer stays put in your view, so turning your head
carries it onto the target; the release clicks there and the correction is a
lesson (judged by the net correction, not the head's path). Held still for
POINTER_GAZE_HOLD it's a real press that the head drags, and Meta+K while
Meta+J aims presses where the dot is now, to correct and then drag.
A Meta combination now also sends the desktop an F24 press and Meta's release
at once: Meta's release no longer opens Plasma's launcher, and the click isn't
Meta+click (KWin's window move and resize, which swallowed right clicks). The
desktop gets Meta back for the next key if it's still held.
Also fixes e16b758, which had taken the gaze actions off the key combination
list along with the controllers'; and gaze_quickcal (the gaze service's
one-dot check) joins the actions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e16b7588e2
commit
dc3a1b8208
4 files changed
+204
-26
No files matched your search
@@ -62,6 +62,9 @@ ACTION_LABELS = {
|
||||
"follow_toggle": "Head follow on/off (experimental)", "gaze_toggle": "Gaze pointer on/off (experimental)",
|
||||
"gaze_precision": "Gaze precision: hold to steer, release to click",
|
||||
"gaze_drag": "Gaze drag: press where you look, steer, release",
|
||||
"gaze_left": "Gaze left click: tap, or hold and turn your head to aim",
|
||||
"gaze_right": "Gaze right click: tap, or hold and turn your head to aim",
|
||||
"gaze_quickcal": "Gaze quick check (one dot)",
|
||||
"sens_up": "Faster pointer",
|
||||
"sens_down": "Slower pointer", "layout_reset": "Reset desktop screen layout",
|
||||
"screens_toggle": "Hide/show desktop screens", "keyboard_toggle": "Open/close keyboard",
|
||||
@@ -87,8 +90,14 @@ CONTROLLER_BUTTONS = {
|
||||
"right/bumper": "Right bumper", "right/trigger": "Right trigger", "right/grip": "Right grip",
|
||||
"right/thumbstick": "Right stick click",
|
||||
}
|
||||
# Not the gaze actions: gaze mode is a mouse feature (docs/gaze-controllers.md; the relay's GAZE_ACTIONS).
|
||||
CONTROLLER_ACTIONS = [a for a in ACTION_LABELS if a not in ("key", "none", "gaze_toggle", "gaze_precision", "gaze_drag")]
|
||||
# Gaze mode is a mouse and keyboard feature (docs/gaze-controllers.md; the relay's GAZE_ACTIONS).
|
||||
GAZE_ACTIONS = ("gaze_toggle", "gaze_precision", "gaze_drag", "gaze_left", "gaze_right", "gaze_quickcal")
|
||||
CONTROLLER_ACTIONS = [a for a in ACTION_LABELS if a not in ("key", "none") + GAZE_ACTIONS]
|
||||
# Key combinations take any action but key and none; the keyboard clicks only work there.
|
||||
SHORTCUT_ACTIONS = [a for a in ACTION_LABELS if a not in ("key", "none")]
|
||||
KEYBOARD_ONLY = ("gaze_left", "gaze_right")
|
||||
# The relay's DEFAULT_KEY_BINDINGS: used while the rules file has no "key_bindings" (Meta+J, Meta+K).
|
||||
DEFAULT_KEY_BINDINGS = {"125+36": "gaze_left", "125+37": "gaze_right"}
|
||||
# Gaze mode settings (pointer helper), like POINTER_SETTINGS.
|
||||
GAZE_SETTINGS = [
|
||||
("POINTER_GAZE_RETAKE", "Look away to hand back", 5, 1, 45, 0.5, "°"),
|
||||
@@ -469,7 +478,7 @@ class Backend(QObject):
|
||||
|
||||
@Property("QVariantList", constant=True)
|
||||
def actions(self):
|
||||
return [{"value": k, "text": v} for k, v in ACTION_LABELS.items()]
|
||||
return [{"value": k, "text": v} for k, v in ACTION_LABELS.items() if k not in KEYBOARD_ONLY]
|
||||
|
||||
@Slot(str, result="QVariantList")
|
||||
def mappings(self, device_id):
|
||||
@@ -842,13 +851,19 @@ class Backend(QObject):
|
||||
|
||||
@Property("QVariantList", notify=mappingsChanged)
|
||||
def keyShortcuts(self):
|
||||
bound = read_json(RULES_PATH).get("key_bindings", {}) or {}
|
||||
bound = self._key_bindings(read_json(RULES_PATH))
|
||||
return [{"combo": c, "label": self.comboName(c), "action": a, "actionLabel": ACTION_LABELS.get(a, a)}
|
||||
for c, a in sorted(bound.items())]
|
||||
|
||||
@Property("QVariantList", constant=True)
|
||||
def shortcutActions(self):
|
||||
return [{"value": a, "text": ACTION_LABELS[a]} for a in CONTROLLER_ACTIONS]
|
||||
return [{"value": a, "text": ACTION_LABELS[a]} for a in SHORTCUT_ACTIONS]
|
||||
|
||||
@staticmethod
|
||||
def _key_bindings(rules):
|
||||
"""The key combinations in effect: the defaults until the rules file has its own."""
|
||||
bound = rules.get("key_bindings")
|
||||
return dict(DEFAULT_KEY_BINDINGS) if bound is None else bound
|
||||
|
||||
@Property(bool, notify=shortcutCaptureChanged)
|
||||
def capturingShortcut(self):
|
||||
@@ -856,7 +871,7 @@ class Backend(QObject):
|
||||
|
||||
@Slot(str)
|
||||
def startShortcutCapture(self, action):
|
||||
if action in CONTROLLER_ACTIONS:
|
||||
if action in SHORTCUT_ACTIONS:
|
||||
self._capture_combo = action
|
||||
self._combo_mods = set()
|
||||
self._send("watch 60")
|
||||
@@ -869,14 +884,16 @@ class Backend(QObject):
|
||||
|
||||
def _save_shortcut(self, combo, action):
|
||||
rules = read_json(RULES_PATH)
|
||||
rules.setdefault("key_bindings", {})[combo] = action
|
||||
rules["key_bindings"] = self._key_bindings(rules) # the defaults stay when the first one is added
|
||||
rules["key_bindings"][combo] = action
|
||||
self._save_rules(rules)
|
||||
self.message.emit(f"{self.comboName(combo)} → {ACTION_LABELS[action]}", False)
|
||||
|
||||
@Slot(str)
|
||||
def removeShortcut(self, combo):
|
||||
rules = read_json(RULES_PATH)
|
||||
(rules.get("key_bindings") or {}).pop(combo, None)
|
||||
rules["key_bindings"] = self._key_bindings(rules)
|
||||
rules["key_bindings"].pop(combo, None)
|
||||
self._save_rules(rules)
|
||||
self.message.emit(f"{self.comboName(combo)} removed", False)
|
||||
|
||||
|
||||
@@ -842,9 +842,11 @@ Kirigami.ApplicationWindow {
|
||||
Controls.Label {
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
|
||||
wrapMode: Text.WordWrap
|
||||
text: "Gaze works with the mouse. The Frame controllers don't take part, and moving one hands the "
|
||||
+ "pointer back to the controllers. Gaze pointer on/off, Gaze precision, and Gaze drag can go on "
|
||||
+ "a mouse button (Buttons page) or a key combination below."
|
||||
text: "Gaze works with the mouse and the keyboard. Keyboard clicks (Meta+J left, Meta+K right by "
|
||||
+ "default): tap to click where you look, or hold, turn your head until the dot sits on the "
|
||||
+ "target, and let go; hold still to drag. The Frame controllers don't take part, and moving "
|
||||
+ "one hands the pointer back to them. Gaze pointer on/off, Gaze precision, and Gaze drag can "
|
||||
+ "also go on a mouse button (Buttons page)."
|
||||
opacity: 0.7
|
||||
font: Kirigami.Theme.smallFont
|
||||
}
|
||||
|
||||
+54
-6
@@ -23,7 +23,11 @@ pointer_toggle, follow_toggle = head follow on or off, gaze_toggle = gaze mode o
|
||||
(the pointer goes where you look; see pointer/helper/ft-pointer.cpp), gaze_precision = while
|
||||
held, the pointer stops where you look and the mouse steers it, and the release clicks there,
|
||||
gaze_drag = the same, but pressed at once, so it drags ("precision|gazedrag mouse|keyboard 1|0"
|
||||
to the helper), sens_up, sens_down,
|
||||
to the helper), gaze_left and gaze_right = keyboard clicks at the gaze: a tap clicks where you
|
||||
look; held, the pointer stops there and your head steers it (it stays put in your view), and
|
||||
the release clicks; held still for half a second, it's a real press that your head drags
|
||||
("gazekey left|right 1|0" to the helper; by default Meta+J and Meta+K, DEFAULT_KEY_BINDINGS),
|
||||
gaze_quickcal = the gaze service's one-dot check ("quickcal" to @ft_gazed), sens_up, sens_down,
|
||||
layout_reset = put the desktop screens back in their saved layout, screens_toggle = hide or show the desktop screens,
|
||||
keyboard_toggle = open or close Frametop's keyboard, key = pass through as a key, none).
|
||||
|
||||
@@ -31,7 +35,10 @@ Frame controller buttons can be mapped too ("controller_buttons": {"right/a": ac
|
||||
rules file; any action but key and the gaze ones, GAZE_ACTIONS: gaze mode is a mouse feature,
|
||||
docs/gaze-controllers.md). So can key combinations on any keyboard ("key_bindings":
|
||||
{"29+56+34": action}, evdev codes joined by "+", modifiers first and left-hand codes for
|
||||
either side, here Ctrl+Alt+G): the combination does the action, and its last key isn't typed. The controllers aren't input devices here, only SteamVR sees
|
||||
either side, here Ctrl+Alt+G): the combination does the action, and its last key isn't typed.
|
||||
A combination with Meta also sends the desktop an F24 press and Meta's release right away: so
|
||||
letting go of Meta doesn't open Plasma's launcher, and a gaze click isn't Meta+click (KWin's
|
||||
window move and resize). Another key while Meta is still held gives the desktop Meta back. The controllers aren't input devices here, only SteamVR sees
|
||||
them, so the pointer helper reads them with SteamVR input and sends "vrbtn <button> 1|0".
|
||||
It only takes the buttons the relay tells it to ("vrbind <button>..." to @ft_pointer_helper,
|
||||
sent on start, reload, and when the helper says "vrhello"), and only while no game runs,
|
||||
@@ -174,10 +181,14 @@ def eviocguniq(length):
|
||||
VIRTUAL_PREFIX = "frametop virtual"
|
||||
RULES_PATH = os.path.expanduser("~/.config/frametop-input.json")
|
||||
ACTIONS = ("left", "right", "middle", "back", "scroll_up", "scroll_down", "dashboard", "recenter",
|
||||
"pointer_toggle", "follow_toggle", "gaze_toggle", "gaze_precision", "gaze_drag", "sens_up", "sens_down",
|
||||
"layout_reset", "screens_toggle", "keyboard_toggle", "key", "none")
|
||||
"pointer_toggle", "follow_toggle", "gaze_toggle", "gaze_precision", "gaze_drag", "gaze_left", "gaze_right",
|
||||
"gaze_quickcal", "sens_up", "sens_down", "layout_reset", "screens_toggle", "keyboard_toggle", "key", "none")
|
||||
# Gaze mode is a mouse feature: these never come from a controller button (docs/gaze-controllers.md).
|
||||
GAZE_ACTIONS = ("gaze_toggle", "gaze_precision", "gaze_drag")
|
||||
GAZE_ACTIONS = ("gaze_toggle", "gaze_precision", "gaze_drag", "gaze_left", "gaze_right", "gaze_quickcal")
|
||||
# Key combinations when the rules file has none: Meta+J and Meta+K click at the gaze (free on the
|
||||
# Frametop desktop, and apps don't use Meta).
|
||||
DEFAULT_KEY_BINDINGS = {"125+36": "gaze_left", "125+37": "gaze_right"}
|
||||
KEY_F24 = 194 # sent to the desktop with a Meta combination (see the top)
|
||||
# Key combinations ("key_bindings"): modifiers, each side's code folded into the left one's.
|
||||
MODIFIERS = {29: 29, 97: 29, 42: 42, 54: 42, 56: 56, 100: 56, 125: 125, 126: 125}
|
||||
VR_KEYBOARD_MODES = ("always", "no_keyboard", "button", "never") # when Frametop's keyboard opens
|
||||
@@ -188,6 +199,7 @@ VR_BUTTONS = ("left/view", "left/dpad_up", "left/dpad_down", "left/dpad_left", "
|
||||
"right/bumper", "right/trigger", "right/grip", "right/thumbstick")
|
||||
VR_DEVICE = "frame_controller" # the id controller buttons have in watch events
|
||||
SCREENS = "\0ft_screens"
|
||||
GAZED = "\0ft_gazed"
|
||||
KEYS = "\0frametop_keys" # keys of keyboards grabbed for the desktop, for other readers
|
||||
FT_LAYOUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "layout", "ft-layout")
|
||||
DEFAULT_BUTTONS = {BTN_LEFT: "left", BTN_RIGHT: "right", BTN_MIDDLE: "middle",
|
||||
@@ -295,7 +307,7 @@ def read_rules(path=RULES_PATH):
|
||||
rules.setdefault("devices", {})
|
||||
rules.setdefault("buttons", {})
|
||||
rules.setdefault("controller_buttons", {})
|
||||
rules.setdefault("key_bindings", {})
|
||||
rules.setdefault("key_bindings", dict(DEFAULT_KEY_BINDINGS))
|
||||
return rules
|
||||
|
||||
|
||||
@@ -437,6 +449,12 @@ class Pointer:
|
||||
which take the mouse or the keyboard only."""
|
||||
if value == 2:
|
||||
return
|
||||
if name in ("gaze_left", "gaze_right"):
|
||||
if value == 1:
|
||||
self.wake(now)
|
||||
self.flush()
|
||||
self.send(f"gazekey {name[5:]} {value}")
|
||||
return
|
||||
if name in ("gaze_precision", "gaze_drag"):
|
||||
if source not in ("mouse", "keyboard"):
|
||||
return # gaze mode is a mouse feature (GAZE_ACTIONS)
|
||||
@@ -474,6 +492,11 @@ class Pointer:
|
||||
elif name == "gaze_toggle":
|
||||
self.send("gaze toggle") # until the next restart; the setting is POINTER_GAZE
|
||||
log("gaze mode toggled")
|
||||
elif name == "gaze_quickcal":
|
||||
try:
|
||||
self.sock.sendto(b"quickcal", GAZED)
|
||||
except OSError:
|
||||
pass # the gaze service isn't running
|
||||
elif name == "screens_toggle":
|
||||
try:
|
||||
self.sock.sendto(b"toggle", SCREENS)
|
||||
@@ -698,14 +721,28 @@ def main():
|
||||
state["pointer"].action(action, value, now, source)
|
||||
|
||||
held_modifiers = set() # on any keyboard, folded (MODIFIERS)
|
||||
held_meta = set() # the Meta keys held, as they are (KEY_LEFTMETA, KEY_RIGHTMETA)
|
||||
meta_hidden = set() # held Meta keys the desktop was told came up (a combination; key_binding)
|
||||
combos_down = {} # key code -> the action its combination started (released with it)
|
||||
|
||||
def key_binding(code, value, now):
|
||||
"""A key from a keyboard: does it complete a key combination ("key_bindings")? True if
|
||||
it was taken for one (then it isn't typed)."""
|
||||
nonlocal meta_down
|
||||
if code in MODIFIERS:
|
||||
(held_modifiers.add if value else held_modifiers.discard)(MODIFIERS[code])
|
||||
if code in (KEY_LEFTMETA, KEY_RIGHTMETA):
|
||||
(held_meta.add if value else held_meta.discard)(code)
|
||||
if value == 0 and code in meta_hidden:
|
||||
meta_hidden.discard(code)
|
||||
return True # the desktop already had it come up (below)
|
||||
return False
|
||||
if value == 1 and code not in combos_down and meta_hidden:
|
||||
# Another key while Meta is still held after a combination: the desktop gets Meta
|
||||
# back first, so Meta+that key still works there.
|
||||
for c in sorted(meta_hidden):
|
||||
to_screens(c, 1)
|
||||
meta_hidden.clear()
|
||||
if value == 0 and code in combos_down:
|
||||
action = combos_down.pop(code)
|
||||
if state["pointer"]:
|
||||
@@ -718,6 +755,17 @@ def main():
|
||||
if action not in ACTIONS or action in ("key", "none"):
|
||||
return False
|
||||
combos_down[code] = action
|
||||
if held_meta - meta_hidden:
|
||||
# The desktop saw Meta go down. Another key in between keeps its release from
|
||||
# opening Plasma's launcher (and Meta from toggling the dashboard here), and Meta
|
||||
# comes up there now: KWin takes Meta with a mouse button for moving or resizing
|
||||
# windows, which would swallow a gaze click. Its real release is dropped (above).
|
||||
meta_down = False
|
||||
to_screens(KEY_F24, 1)
|
||||
to_screens(KEY_F24, 0)
|
||||
for c in sorted(held_meta - meta_hidden):
|
||||
to_screens(c, 0)
|
||||
meta_hidden.update(held_meta)
|
||||
if state["pointer"]:
|
||||
do_action(action, 1, now, "keyboard")
|
||||
log(f"key combination {combo}: {action}")
|
||||
|
||||
@@ -175,6 +175,18 @@
|
||||
// pointer is.
|
||||
// In gaze mode the mouse's left button is a gaze precision button (POINTER_GAZE_MOUSE =
|
||||
// precision, the default), or clicks at once where the pointer is (direct).
|
||||
// Keyboard clicks (the relay's gaze_left and gaze_right, Meta+J and Meta+K by default;
|
||||
// "gazekey left|right 1|0" here) work like gaze mode's mouse press, steered by the head: the
|
||||
// press stops the pointer where you look, and while the keys are held the pointer stays put in
|
||||
// your view, so turning your head carries it onto what you meant (past
|
||||
// POINTER_HEAD_DEADZONE, 0.5 deg, so a still head doesn't wobble it). The release clicks there
|
||||
// (left, or right for gaze_right), and a correction is a lesson for the gaze tracker, as with
|
||||
// the mouse. Held still for POINTER_GAZE_HOLD instead, it's a real press, and the head drags.
|
||||
// A quick tap (let go within POINTER_KEY_TAP, 0.25 s) clicks where the dot was at the press,
|
||||
// whatever the head did meanwhile, and tells the gaze tracker it was right there (a lesson
|
||||
// with no correction). Pressing gaze_right while gaze_left aims (Meta+K with Meta+J held)
|
||||
// presses the left button where the dot is now, so you can correct first and then drag; the
|
||||
// first key let go drops it. Without gaze mode they work from wherever the pointer is.
|
||||
// POINTER_ROLE (right, left, or stylus): the hand role our device takes while connected. A
|
||||
// Frame controller in your hand counts as used through its touch sensors and takes its hand's
|
||||
// role back, and then no click lands (see "no hand role" in the main loop): with a controller
|
||||
@@ -257,7 +269,8 @@
|
||||
// move to take the laser back, above. POINTER_IGNORE (empty): ignored panels, above.
|
||||
// POINTER_HANDS (0), POINTER_PINCH_GAIN (0.5), POINTER_PINCH_DEADZONE (1.5 deg),
|
||||
// POINTER_GRIP_GAIN (1), POINTER_GRIP_BELOW (0.35 m), POINTER_PINCH_TYPING (1 s): hands, above.
|
||||
// POINTER_GAZE_MOUSE (precision), POINTER_ROLE (right): gaze precision, above.
|
||||
// POINTER_GAZE_MOUSE (precision), POINTER_HEAD_DEADZONE (0.5 deg), POINTER_KEY_TAP (0.25 s),
|
||||
// POINTER_ROLE (right): gaze precision and keyboard clicks, above.
|
||||
#include <openvr.h>
|
||||
|
||||
#include "vrbuttons.h"
|
||||
@@ -666,6 +679,7 @@ int main() {
|
||||
bool gazeOn = false, gazeConf = false;
|
||||
double gazeRetake = 5, gazeNudgeMax = 8, gazeHold = 0.5, gazeShow = 1;
|
||||
bool gazeDotAlways = true; // POINTER_GAZE_DOT (see the top)
|
||||
double headDeadzone = 0.5, keyTap = 0.25; // POINTER_HEAD_DEADZONE, POINTER_KEY_TAP (keyboard clicks, see the top)
|
||||
// Hands (see the top): POINTER_HANDS, POINTER_PINCH_GAIN, POINTER_PINCH_DEADZONE, POINTER_GRIP_GAIN.
|
||||
bool handsOn = false;
|
||||
double pinchGain = 0.5, pinchDeadzone = 1.5, gripGain = 1.0, handBelow = 0.35, typingHold = 1.0;
|
||||
@@ -695,6 +709,8 @@ int main() {
|
||||
gazeNudgeMax = std::clamp(ConfDouble(conf, "POINTER_GAZE_NUDGE_MAX", 8), 1.0, 30.0);
|
||||
gazeHold = std::clamp(ConfDouble(conf, "POINTER_GAZE_HOLD", 0.5), 0.1, 5.0);
|
||||
gazeShow = std::clamp(ConfDouble(conf, "POINTER_GAZE_SHOW", 1), 0.0, 30.0);
|
||||
headDeadzone = std::clamp(ConfDouble(conf, "POINTER_HEAD_DEADZONE", 0.5), 0.0, 5.0);
|
||||
keyTap = std::clamp(ConfDouble(conf, "POINTER_KEY_TAP", 0.25), 0.0, 1.0);
|
||||
const auto gd = conf.find("POINTER_GAZE_DOT");
|
||||
gazeDotAlways = gd == conf.end() || gd->second != "moving";
|
||||
const bool wantGaze = ConfDouble(conf, "POINTER_GAZE", 0) != 0;
|
||||
@@ -844,13 +860,17 @@ int main() {
|
||||
// hand: seen from the eye then, in the room), and the pointer then.
|
||||
HandGestures handFile;
|
||||
PoseHistory poses;
|
||||
enum class Src { None, Hand, Mouse };
|
||||
enum class Src { None, Hand, Mouse, Head };
|
||||
struct Hold {
|
||||
Src src = Src::None;
|
||||
int side = -1; // a hand's side (0 left, 1 right)
|
||||
bool grip = false; // pressed at once and dragging (a grip, gaze drag), not a click on release
|
||||
bool engaged = false; // past the dead zone
|
||||
bool pressed = false; // a real press went out (a grip or gaze drag, or a pinch without gaze mode)
|
||||
bool promote = false; // held still for POINTER_GAZE_HOLD, it becomes a real press (keyboard clicks)
|
||||
bool right = false; // the right button (gaze_right)
|
||||
bool keyDrag = false; // gaze_right pressed while gaze_left aimed: a left press, either key ends it
|
||||
double pressYaw = 0, pressPitch = 0; // the pointer at the press (a quick tap clicks there)
|
||||
Vec3 origin;
|
||||
double refYaw = 0, refPitch = 0, startYaw = 0, startPitch = 0, lastYaw = 0, lastPitch = 0;
|
||||
} hold;
|
||||
@@ -860,6 +880,11 @@ int main() {
|
||||
bool drag, down;
|
||||
};
|
||||
std::vector<DevicePress> devicePresses;
|
||||
// "gazekey left|right 1|0" from the relay (keyboard clicks), done in the frame.
|
||||
struct KeyPress {
|
||||
bool right, down;
|
||||
};
|
||||
std::vector<KeyPress> keyPresses;
|
||||
uint32_t seenBegins[2][2] = {}, seenEnds[2][2] = {}; // [pinch, grip][side], as last read
|
||||
bool handBaseline = false;
|
||||
int handOpens = 0;
|
||||
@@ -870,14 +895,18 @@ int main() {
|
||||
bool inGame = false, gazeAwake = false;
|
||||
Clock::time_point inGameAt{}, gazeAwakeAt{};
|
||||
Clock::time_point lastMove{}, lastHeld{}, pulseAt{};
|
||||
// The left button, as sent to the driver.
|
||||
// The left button, as sent to the driver; pressRight: the next press is the right button
|
||||
// instead (gaze_right), heldButton: the one pressed.
|
||||
bool pressRight = false;
|
||||
std::string heldButton = "trigger";
|
||||
bool confirmLesson = false; // a keyboard click's quick tap: a lesson with no correction (see the top)
|
||||
auto pressLeft = [&] {
|
||||
// ft-screens sends the keyboard to the panel clicked last; it sees clicks on
|
||||
// its own screens, but only we know when one lands on another panel.
|
||||
SendTo(out, "ft_screens", "click " + (lastHit.empty() ? std::string("-") : lastHit));
|
||||
// A click after nudging the gaze-placed pointer: the nudge is a lesson (see the top).
|
||||
if (gazeOn && nudging && !gazeOwns && havePoint && Clock::now() - nudgeAt < std::chrono::seconds(10) &&
|
||||
nudgeMoved >= 0.2 && nudgeMoved <= gazeNudgeMax) {
|
||||
(confirmLesson || (nudgeMoved >= 0.2 && nudgeMoved <= gazeNudgeMax))) {
|
||||
const Vec3 d = RotateInverse(nudgeHead, Normalize(lastPoint - Position(nudgeHead)));
|
||||
char msg[160];
|
||||
std::snprintf(msg, sizeof msg, "lesson %.3f %.3f %.3f %.3f", nudgeRawHy, nudgeRawHp,
|
||||
@@ -886,7 +915,7 @@ int main() {
|
||||
if (debug) std::printf("gaze %s (nudged %.2f deg)\n", msg, nudgeMoved);
|
||||
if (debug) std::fflush(stdout);
|
||||
}
|
||||
nudging = false;
|
||||
nudging = confirmLesson = false;
|
||||
leftHeld = true;
|
||||
dragDistance = lastDistance;
|
||||
pressKey.clear();
|
||||
@@ -900,14 +929,16 @@ int main() {
|
||||
tiltYaw = tiltPitch = 0; // a new drag starts untilted
|
||||
dropHoldUntil = {};
|
||||
pulseAt = Clock::now();
|
||||
SendTo(out, "ft_pointer", "btn trigger 1");
|
||||
heldButton = pressRight ? "b" : "trigger";
|
||||
pressRight = false;
|
||||
SendTo(out, "ft_pointer", "btn " + heldButton + " 1");
|
||||
};
|
||||
auto releaseLeft = [&] {
|
||||
leftHeld = false;
|
||||
tilting = false;
|
||||
// Hold the drag pose (tilt, frozen distance) while SteamVR finishes the drop.
|
||||
dropHoldUntil = Clock::now() + std::chrono::milliseconds(500);
|
||||
SendTo(out, "ft_pointer", "btn trigger 0");
|
||||
SendTo(out, "ft_pointer", "btn " + heldButton + " 0");
|
||||
// ft-screens releases a button held on its screens in KWin even when SteamVR hands
|
||||
// the release to some other overlay (its catcher usually gets it; this is the backstop).
|
||||
SendTo(out, "ft_screens", "up");
|
||||
@@ -1167,6 +1198,13 @@ int main() {
|
||||
devicePresses.push_back({source, !std::strcmp(kind, "gazedrag"), v != 0});
|
||||
continue;
|
||||
}
|
||||
if (std::sscanf(buf, "gazekey %15s %d", source, &v) == 2 &&
|
||||
(!std::strcmp(source, "left") || !std::strcmp(source, "right"))) {
|
||||
lastMouse = Clock::now();
|
||||
if (!active) wake(Clock::now());
|
||||
keyPresses.push_back({!std::strcmp(source, "right"), v != 0});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (std::strcmp(buf, "typing") == 0) { // not mouse input: it never wakes the pointer
|
||||
lastTyping = Clock::now();
|
||||
@@ -1454,11 +1492,14 @@ int main() {
|
||||
if (gazeOn) gazeOwns = true, nudging = false; // no click: the pointer goes back to the gaze
|
||||
} else {
|
||||
clickPress = true; // the click is on the release, where the pointer is now
|
||||
pressRight = hold.right;
|
||||
gazeBack = nudgeMoved < 0.2;
|
||||
}
|
||||
}
|
||||
if (debug)
|
||||
std::printf("hold %s %s%s\n", hold.src == Src::Hand ? (hold.grip ? "grip" : "pinch") : hold.grip ? "gaze drag" : "gaze precision",
|
||||
std::printf("hold %s %s%s\n", hold.src == Src::Hand ? (hold.grip ? "grip" : "pinch")
|
||||
: hold.src == Src::Head ? (hold.right ? "key right" : "key left")
|
||||
: hold.grip ? "gaze drag" : "gaze precision",
|
||||
lost ? "lost" : "released", hold.pressed ? "" : lost ? " (no click)" : " (click)");
|
||||
if (debug) std::fflush(stdout);
|
||||
hold = {};
|
||||
@@ -1493,7 +1534,12 @@ int main() {
|
||||
}
|
||||
yaw = std::remainder(hold.startYaw + gain * dy, 360.0);
|
||||
pitch = std::clamp(hold.startPitch + gain * dp, -85.0, 85.0);
|
||||
if (!hold.pressed) nudgeMoved += std::hypot(std::remainder(yaw - hold.lastYaw, 360.0), pitch - hold.lastPitch);
|
||||
// How far the nudge went: for a keyboard click, from where the dot was at the press
|
||||
// (a head wobbles on the way); otherwise the path, as with the mouse.
|
||||
if (!hold.pressed && hold.src == Src::Head)
|
||||
nudgeMoved = std::hypot(std::remainder(yaw - hold.pressYaw, 360.0), pitch - hold.pressPitch);
|
||||
else if (!hold.pressed)
|
||||
nudgeMoved += std::hypot(std::remainder(yaw - hold.lastYaw, 360.0), pitch - hold.lastPitch);
|
||||
hold.lastYaw = yaw, hold.lastPitch = pitch;
|
||||
lastMove = tnow; // the dot shows while it moves (gaze mode)
|
||||
};
|
||||
@@ -1551,6 +1597,54 @@ int main() {
|
||||
if (debug) std::fflush(stdout);
|
||||
}
|
||||
devicePresses.clear();
|
||||
// Keyboard clicks (see the top): held back at the gaze, steered by the head.
|
||||
auto headAngles = [&](double &hy, double &hp) {
|
||||
if (!hmd.bPoseIsValid) return false;
|
||||
const Vec3 f = Normalize({-hm[0][2], -hm[1][2], -hm[2][2]});
|
||||
hy = std::atan2(-f.x, -f.z) * 180 / M_PI;
|
||||
hp = std::asin(std::clamp(f.y, -1.0, 1.0)) * 180 / M_PI;
|
||||
return true;
|
||||
};
|
||||
for (const KeyPress &k : keyPresses) {
|
||||
if (!k.down) {
|
||||
if (hold.src == Src::Head && (hold.right == k.right || hold.keyDrag)) {
|
||||
if (!hold.pressed && aimHeld && tnow - aimSince < std::chrono::duration<double>(keyTap)) {
|
||||
// A quick tap: a click where the dot was at the press, and the gaze was right.
|
||||
yaw = hold.pressYaw, pitch = hold.pressPitch;
|
||||
nudgeMoved = 0;
|
||||
confirmLesson = true;
|
||||
}
|
||||
endHold(false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (k.right && hold.src == Src::Head && !hold.right && !hold.pressed && aimHeld) {
|
||||
// gaze_right while gaze_left aims: press the left button where the dot is now
|
||||
// (the correction is a lesson), then the head drags.
|
||||
aimHeld = aimHand = false;
|
||||
hold.pressed = hold.grip = hold.keyDrag = true, hold.promote = false, hold.engaged = false;
|
||||
headAngles(hold.refYaw, hold.refPitch);
|
||||
gazeBack = gazeOn;
|
||||
pressRight = false;
|
||||
pressLeft();
|
||||
if (debug) std::printf("hold key left: pressed (right key)\n");
|
||||
if (debug) std::fflush(stdout);
|
||||
continue;
|
||||
}
|
||||
if (hold.src != Src::None || leftHeld || aimHeld || clickPress || clickRelease) continue;
|
||||
Hold h;
|
||||
h.src = Src::Head, h.promote = true, h.right = k.right, h.pressYaw = yaw, h.pressPitch = pitch;
|
||||
if (!headAngles(h.refYaw, h.refPitch)) continue;
|
||||
hold = h;
|
||||
startHold(false);
|
||||
if (debug) std::printf("hold key %s began\n", k.right ? "right" : "left");
|
||||
if (debug) std::fflush(stdout);
|
||||
}
|
||||
keyPresses.clear();
|
||||
if (hold.src == Src::Head) {
|
||||
double hy, hp;
|
||||
if (headAngles(hy, hp)) steer(hy, hp, hold.pressed ? 0.3 : headDeadzone, 1.0);
|
||||
}
|
||||
fh_gestures_t hg;
|
||||
const bool handOk = handsOn && handFile.Read(hg);
|
||||
if (handOk && (hg.seq != handSeq || handFile.opens != handOpens)) {
|
||||
@@ -1951,6 +2045,23 @@ int main() {
|
||||
gazeBack = true;
|
||||
pressLeft();
|
||||
}
|
||||
// A keyboard click held still for POINTER_GAZE_HOLD: a real press, then the head drags
|
||||
// (from where it is now).
|
||||
if (aimHeld && aimHand && hold.promote && !hold.engaged &&
|
||||
tnow - aimSince >= std::chrono::duration<double>(gazeHold)) {
|
||||
aimHeld = aimHand = false;
|
||||
hold.pressed = hold.grip = true;
|
||||
if (hmd.bPoseIsValid) {
|
||||
const Vec3 f = Normalize({-hm[0][2], -hm[1][2], -hm[2][2]});
|
||||
hold.refYaw = std::atan2(-f.x, -f.z) * 180 / M_PI;
|
||||
hold.refPitch = std::asin(std::clamp(f.y, -1.0, 1.0)) * 180 / M_PI;
|
||||
}
|
||||
gazeBack = gazeOn;
|
||||
pressRight = hold.right;
|
||||
pressLeft();
|
||||
if (debug) std::printf("hold key %s: pressed (held still)\n", hold.right ? "right" : "left");
|
||||
if (debug) std::fflush(stdout);
|
||||
}
|
||||
if (clickPress) {
|
||||
clickPress = false;
|
||||
pressLeft();
|
||||
|
||||
Reference in new issue
Block a user