From 4906f8105f9f831b5977d4935f90ef26b8afdafd Mon Sep 17 00:00:00 2001 From: Jules Aguillon Date: Sat, 25 May 2024 21:19:44 +0200 Subject: [PATCH] Circle and round trip gestures (#640) This implements clockwise/anticlockwise circle and round trip gestures inspired by Messagease. The circle gestures start after a small threshold to avoid making the regular swipe too hard to aim. The gestures do: - circle: The center symbol with Shift applied, with a fallback on Fn - round trip: Same as the circle gesture but applied to a side symbol - anticlockwise circle: Nothing currently. It is intended to be made configurable per-layout in the future. The new Gesture class keeps track of what the pointer is doing while it moves on a key. It replaces the 'selected_direction' integer. --- srcs/juloo.keyboard2/Gesture.java | 141 +++++++++++++++++++++++++ srcs/juloo.keyboard2/KeyModifier.java | 18 +++- srcs/juloo.keyboard2/KeyValue.java | 11 +- srcs/juloo.keyboard2/Pointers.java | 146 ++++++++++++++++++++------ 4 files changed, 274 insertions(+), 42 deletions(-) create mode 100644 srcs/juloo.keyboard2/Gesture.java diff --git a/srcs/juloo.keyboard2/Gesture.java b/srcs/juloo.keyboard2/Gesture.java new file mode 100644 index 0000000..5ee666b --- /dev/null +++ b/srcs/juloo.keyboard2/Gesture.java @@ -0,0 +1,141 @@ +package juloo.keyboard2; + +public final class Gesture +{ + /** The pointer direction that caused the last state change. + Integer from 0 to 15 (included). */ + int current_dir; + + State state; + + public Gesture(int starting_direction) + { + current_dir = starting_direction; + state = State.Swiped; + } + + enum State + { + Cancelled, + Swiped, + Rotating_clockwise, + Rotating_anticlockwise, + Ended_swipe, + Ended_center, + Ended_clockwise, + Ended_anticlockwise + } + + enum Name + { + None, + Swipe, + Roundtrip, + Circle, + Anticircle + } + + /** Angle to travel before a rotation gesture starts. A threshold too low + would be too easy to reach while doing back and forth gestures, as the + quadrants are very small. In the same unit as [current_dir] */ + static final int ROTATION_THRESHOLD = 2; + + /** Return the currently recognized gesture. Return [null] if no gesture is + recognized. Might change everytime [changed_direction] return [true]. */ + public Name get_gesture() + { + switch (state) + { + case Cancelled: + return Name.None; + case Swiped: + case Ended_swipe: + return Name.Swipe; + case Ended_center: + return Name.Roundtrip; + case Rotating_clockwise: + case Ended_clockwise: + return Name.Circle; + case Rotating_anticlockwise: + case Ended_anticlockwise: + return Name.Anticircle; + } + return Name.None; // Unreachable + } + + public boolean is_in_progress() + { + switch (state) + { + case Swiped: + case Rotating_clockwise: + case Rotating_anticlockwise: + return true; + } + return false; + } + + public int current_direction() { return current_dir; } + + /** The pointer changed direction. Return [true] if the gesture changed + state and [get_gesture] return a different value. */ + public boolean changed_direction(int direction) + { + int d = dir_diff(current_dir, direction); + boolean clockwise = d > 0; + switch (state) + { + case Swiped: + if (Math.abs(d) < ROTATION_THRESHOLD) + return false; + // Start a rotation + state = (clockwise) ? + State.Rotating_clockwise : State.Rotating_anticlockwise; + current_dir = direction; + return true; + // Check that rotation is not reversing + case Rotating_clockwise: + case Rotating_anticlockwise: + current_dir = direction; + if ((state == State.Rotating_clockwise) == clockwise) + return false; + state = State.Cancelled; + return true; + } + return false; + } + + /** Return [true] if [get_gesture] will return a different value. */ + public boolean moved_to_center() + { + switch (state) + { + case Swiped: state = State.Ended_center; return true; + case Rotating_clockwise: state = State.Ended_clockwise; return false; + case Rotating_anticlockwise: state = State.Ended_anticlockwise; return false; + } + return false; + } + + /** Will not change the gesture state. */ + public void pointer_up() + { + switch (state) + { + case Swiped: state = State.Ended_swipe; break; + case Rotating_clockwise: state = State.Ended_clockwise; break; + case Rotating_anticlockwise: state = State.Ended_anticlockwise; break; + } + } + + static int dir_diff(int d1, int d2) + { + final int n = 16; + // Shortest-path in modulo arithmetic + if (d1 == d2) + return 0; + int left = (d1 - d2 + n) % n; + int right = (d2 - d1 + n) % n; + return (left < right) ? -left : right; + } +} diff --git a/srcs/juloo.keyboard2/KeyModifier.java b/srcs/juloo.keyboard2/KeyModifier.java index f358e97..a320ec5 100644 --- a/srcs/juloo.keyboard2/KeyModifier.java +++ b/srcs/juloo.keyboard2/KeyModifier.java @@ -33,7 +33,6 @@ public final class KeyModifier if (r == null) { r = k; - /* Order: Fn, Shift, accents */ for (int i = 0; i < n_mods; i++) r = modify(r, mods.get(i)); ks.put(mods, r); @@ -70,6 +69,7 @@ public final class KeyModifier case ALT: case META: return turn_into_keyevent(k); case FN: return apply_fn(k); + case GESTURE: return apply_gesture(k); case SHIFT: return apply_shift(k); case GRAVE: return apply_map_char(k, map_char_grave); case AIGU: return apply_map_char(k, map_char_aigu); @@ -139,11 +139,10 @@ public final class KeyModifier case Char: char kc = k.getChar(); String modified = map.apply(kc); - if (modified == null) - return k; - return KeyValue.makeStringKey(modified, k.getFlags()); - default: return k; + if (modified != null) + return KeyValue.makeStringKey(modified, k.getFlags()); } + return k; } private static KeyValue apply_shift(KeyValue k) @@ -482,6 +481,15 @@ public final class KeyModifier return k.withKeyevent(e); } + /** Modify a key affected by a round-trip or a clockwise circle gesture. */ + private static KeyValue apply_gesture(KeyValue k) + { + KeyValue shifted = apply_shift(k); + if (shifted == null || shifted.equals(k)) + return apply_fn(k); + return shifted; + } + /* Lookup the cache entry for a key. Create it needed. */ private static HashMap cacheEntry(KeyValue k) { diff --git a/srcs/juloo.keyboard2/KeyValue.java b/srcs/juloo.keyboard2/KeyValue.java index 8435cae..e9e66cc 100644 --- a/srcs/juloo.keyboard2/KeyValue.java +++ b/srcs/juloo.keyboard2/KeyValue.java @@ -27,6 +27,7 @@ public final class KeyValue implements Comparable public static enum Modifier { SHIFT, + GESTURE, CTRL, ALT, META, @@ -54,8 +55,8 @@ public final class KeyValue implements Comparable ARROW_RIGHT, BREVE, BAR, - FN, // Must be placed last to be applied first - } + FN, + } // Last is be applied first public static enum Editing { @@ -404,6 +405,12 @@ public final class KeyValue implements Comparable return new KeyValue(str, Kind.String, 0, flags | FLAG_SMALLER_FONT); } + /** Make a modifier key for passing to [KeyModifier]. */ + public static KeyValue makeInternalModifier(Modifier mod) + { + return new KeyValue("", Kind.Modifier, mod.ordinal(), 0); + } + public static KeyValue getKeyByName(String name) { switch (name) diff --git a/srcs/juloo.keyboard2/Pointers.java b/srcs/juloo.keyboard2/Pointers.java index 4d6ff9a..8de4832 100644 --- a/srcs/juloo.keyboard2/Pointers.java +++ b/srcs/juloo.keyboard2/Pointers.java @@ -143,6 +143,12 @@ public final class Pointers implements Handler.Callback return; } stopKeyRepeat(ptr); + KeyValue ptr_value = ptr.value; + if (ptr.gesture != null && ptr.gesture.is_in_progress()) + { + // A gesture was in progress + ptr.gesture.pointer_up(); + } Pointer latched = getLatched(ptr); if (latched != null) // Already latched { @@ -152,7 +158,7 @@ public final class Pointers implements Handler.Callback else // Otherwise, unlatch { removePtr(latched); - _handler.onPointerUp(ptr.value, ptr.modifiers); + _handler.onPointerUp(ptr_value, ptr.modifiers); } } else if ((ptr.flags & FLAG_P_LATCHABLE) != 0) @@ -168,7 +174,7 @@ public final class Pointers implements Handler.Callback { clearLatched(); removePtr(ptr); - _handler.onPointerUp(ptr.value, ptr.modifiers); + _handler.onPointerUp(ptr_value, ptr.modifiers); } } @@ -217,18 +223,15 @@ public final class Pointers implements Handler.Callback return k.keys[DIRECTION_TO_INDEX[direction]]; } - /* - * Get the KeyValue at the given direction. In case of swipe (direction != - * null), get the nearest KeyValue that is not key0. - * Take care of applying [_handler.onPointerSwipe] to the selected key, this - * must be done at the same time to be sure to treat removed keys correctly. - * Return [null] if no key could be found in the given direction or if the - * selected key didn't change. + /** + * Get the key nearest to [direction] that is not key0. Take care + * of applying [_handler.modifyKey] to the selected key in the same + * operation to be sure to treat removed keys correctly. + * Return [null] if no key could be found in the given direction or + * if the selected key didn't change. */ - private KeyValue getNearestKeyAtDirection(Pointer ptr, Integer direction) + private KeyValue getNearestKeyAtDirection(Pointer ptr, int direction) { - if (direction == null) - return _handler.modifyKey(ptr.key.keys[0], ptr.modifiers); KeyValue k; // [i] is [0, -1, 1, -2, 2, ...] for (int i = 0; i > -4; i = (~i>>31) - i) @@ -261,37 +264,59 @@ public final class Pointers implements Handler.Callback float dy = y - ptr.downY; float dist = Math.abs(dx) + Math.abs(dy); - Integer direction; if (dist < _config.swipe_dist_px) { - direction = null; + // Pointer is still on the center. + if (ptr.gesture == null || !ptr.gesture.is_in_progress()) + return; + // Gesture ended + ptr.gesture.moved_to_center(); + ptr.value = apply_gesture(ptr, ptr.gesture.get_gesture()); + ptr.flags = 0; + } else - { + { // Pointer is on a quadrant. // See [getKeyAtDirection()] for the meaning. The starting point on the // circle is the top direction. double a = Math.atan2(dy, dx) + Math.PI; // a is between 0 and 2pi, 0 is pointing to the left // add 12 to align 0 to the top - direction = ((int)(a * 8 / Math.PI) + 12) % 16; - } + int direction = ((int)(a * 8 / Math.PI) + 12) % 16; + if (ptr.gesture == null) + { // Gesture starts - if (direction != ptr.selected_direction) - { - ptr.selected_direction = direction; - KeyValue newValue = getNearestKeyAtDirection(ptr, direction); - if (newValue != null && !newValue.equals(ptr.value)) - { - ptr.value = newValue; - ptr.flags = pointer_flags_of_kv(newValue); - // Sliding mode is entered when key5 or key6 is down on a slider key. - if (ptr.key.slider && - (newValue.equals(ptr.key.getKeyValue(5)) - || newValue.equals(ptr.key.getKeyValue(6)))) - { - startSliding(ptr, x); + ptr.gesture = new Gesture(direction); + KeyValue new_value = getNearestKeyAtDirection(ptr, direction); + if (new_value != null) + { // Pointer is swiping into a side key. + + ptr.value = new_value; + ptr.flags = pointer_flags_of_kv(new_value); + // Sliding mode is entered when key5 or key6 is down on a slider key. + if (ptr.key.slider && + (new_value.equals(ptr.key.getKeyValue(5)) + || new_value.equals(ptr.key.getKeyValue(6)))) + { + startSliding(ptr, x); + } + _handler.onPointerDown(new_value, true); + } + + } + else if (ptr.gesture.changed_direction(direction)) + { // Gesture changed state + if (!ptr.gesture.is_in_progress()) + { // Gesture ended + stopKeyRepeat(ptr); + _handler.onPointerFlagsChanged(true); + } + else + { + ptr.value = apply_gesture(ptr, ptr.gesture.get_gesture()); + restartKeyRepeat(ptr); + ptr.flags = 0; // Special behaviors are ignored during a gesture. } - _handler.onPointerDown(newValue, true); } } } @@ -395,6 +420,12 @@ public final class Pointers implements Handler.Callback } } + private void restartKeyRepeat(Pointer ptr) + { + stopKeyRepeat(ptr); + startKeyRepeat(ptr); + } + /** A pointer is repeating. Returns [true] if repeat should continue. */ private boolean handleKeyRepeat(Pointer ptr) { @@ -447,14 +478,51 @@ public final class Pointers implements Handler.Callback return flags; } + // Gestures + + /** Apply a gesture to the current key. */ + KeyValue apply_gesture(Pointer ptr, Gesture.Name gesture) + { + switch (gesture) + { + case None: + return ptr.value; + case Swipe: + return ptr.value; + case Roundtrip: + return + modify_key_with_extra_modifier( + ptr, + getNearestKeyAtDirection(ptr, ptr.gesture.current_direction()), + KeyValue.Modifier.GESTURE); + case Circle: + return + modify_key_with_extra_modifier(ptr, ptr.key.keys[0], + KeyValue.Modifier.GESTURE); + case Anticircle: + return _handler.modifyKey(ptr.key.keys[0], ptr.modifiers); + } + return ptr.value; // Unreachable + } + + KeyValue modify_key_with_extra_modifier(Pointer ptr, KeyValue kv, + KeyValue.Modifier extra_mod) + { + return + _handler.modifyKey(kv, + ptr.modifiers.with_extra_mod(KeyValue.makeInternalModifier(extra_mod))); + } + + // Pointers + private static final class Pointer { /** -1 when latched. */ public int pointerId; /** The Key pressed by this Pointer */ public final KeyboardData.Key key; - /** Current direction. [null] means not swiping. */ - public Integer selected_direction; + /** Gesture state, see [Gesture]. [null] means the pointer has not moved out of the center region. */ + public Gesture gesture; /** Selected value with [modifiers] applied. */ public KeyValue value; public float downX; @@ -472,7 +540,7 @@ public final class Pointers implements Handler.Callback { pointerId = p; key = k; - selected_direction = null; + gesture = null; value = v; downX = x; downY = y; @@ -602,6 +670,14 @@ public final class Pointers implements Handler.Callback return false; } + /** Return a copy of this object with an extra modifier added. */ + public Modifiers with_extra_mod(KeyValue m) + { + KeyValue[] newmods = Arrays.copyOf(_mods, _size + 1); + newmods[_size] = m; + return ofArray(newmods, newmods.length); + } + /** Returns the activated modifiers that are not in [m2]. */ public Iterator diff(Modifiers m2) {