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.
This commit is contained in:
Jules Aguillon 2024-05-25 21:19:44 +02:00 committed by GitHub
parent 96fc4003f1
commit 4906f8105f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 274 additions and 42 deletions

View File

@ -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;
}
}

View File

@ -33,7 +33,6 @@ public final class KeyModifier
if (r == null) if (r == null)
{ {
r = k; r = k;
/* Order: Fn, Shift, accents */
for (int i = 0; i < n_mods; i++) for (int i = 0; i < n_mods; i++)
r = modify(r, mods.get(i)); r = modify(r, mods.get(i));
ks.put(mods, r); ks.put(mods, r);
@ -70,6 +69,7 @@ public final class KeyModifier
case ALT: case ALT:
case META: return turn_into_keyevent(k); case META: return turn_into_keyevent(k);
case FN: return apply_fn(k); case FN: return apply_fn(k);
case GESTURE: return apply_gesture(k);
case SHIFT: return apply_shift(k); case SHIFT: return apply_shift(k);
case GRAVE: return apply_map_char(k, map_char_grave); case GRAVE: return apply_map_char(k, map_char_grave);
case AIGU: return apply_map_char(k, map_char_aigu); case AIGU: return apply_map_char(k, map_char_aigu);
@ -139,11 +139,10 @@ public final class KeyModifier
case Char: case Char:
char kc = k.getChar(); char kc = k.getChar();
String modified = map.apply(kc); String modified = map.apply(kc);
if (modified == null) if (modified != null)
return k; return KeyValue.makeStringKey(modified, k.getFlags());
return KeyValue.makeStringKey(modified, k.getFlags());
default: return k;
} }
return k;
} }
private static KeyValue apply_shift(KeyValue k) private static KeyValue apply_shift(KeyValue k)
@ -482,6 +481,15 @@ public final class KeyModifier
return k.withKeyevent(e); 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. */ /* Lookup the cache entry for a key. Create it needed. */
private static HashMap<Pointers.Modifiers, KeyValue> cacheEntry(KeyValue k) private static HashMap<Pointers.Modifiers, KeyValue> cacheEntry(KeyValue k)
{ {

View File

@ -27,6 +27,7 @@ public final class KeyValue implements Comparable<KeyValue>
public static enum Modifier public static enum Modifier
{ {
SHIFT, SHIFT,
GESTURE,
CTRL, CTRL,
ALT, ALT,
META, META,
@ -54,8 +55,8 @@ public final class KeyValue implements Comparable<KeyValue>
ARROW_RIGHT, ARROW_RIGHT,
BREVE, BREVE,
BAR, BAR,
FN, // Must be placed last to be applied first FN,
} } // Last is be applied first
public static enum Editing public static enum Editing
{ {
@ -404,6 +405,12 @@ public final class KeyValue implements Comparable<KeyValue>
return new KeyValue(str, Kind.String, 0, flags | FLAG_SMALLER_FONT); 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) public static KeyValue getKeyByName(String name)
{ {
switch (name) switch (name)

View File

@ -143,6 +143,12 @@ public final class Pointers implements Handler.Callback
return; return;
} }
stopKeyRepeat(ptr); 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); Pointer latched = getLatched(ptr);
if (latched != null) // Already latched if (latched != null) // Already latched
{ {
@ -152,7 +158,7 @@ public final class Pointers implements Handler.Callback
else // Otherwise, unlatch else // Otherwise, unlatch
{ {
removePtr(latched); removePtr(latched);
_handler.onPointerUp(ptr.value, ptr.modifiers); _handler.onPointerUp(ptr_value, ptr.modifiers);
} }
} }
else if ((ptr.flags & FLAG_P_LATCHABLE) != 0) else if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
@ -168,7 +174,7 @@ public final class Pointers implements Handler.Callback
{ {
clearLatched(); clearLatched();
removePtr(ptr); 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]]; return k.keys[DIRECTION_TO_INDEX[direction]];
} }
/* /**
* Get the KeyValue at the given direction. In case of swipe (direction != * Get the key nearest to [direction] that is not key0. Take care
* null), get the nearest KeyValue that is not key0. * of applying [_handler.modifyKey] to the selected key in the same
* Take care of applying [_handler.onPointerSwipe] to the selected key, this * operation to be sure to treat removed keys correctly.
* 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
* Return [null] if no key could be found in the given direction or if the * if the selected key didn't change.
* 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; KeyValue k;
// [i] is [0, -1, 1, -2, 2, ...] // [i] is [0, -1, 1, -2, 2, ...]
for (int i = 0; i > -4; i = (~i>>31) - i) 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 dy = y - ptr.downY;
float dist = Math.abs(dx) + Math.abs(dy); float dist = Math.abs(dx) + Math.abs(dy);
Integer direction;
if (dist < _config.swipe_dist_px) 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 else
{ { // Pointer is on a quadrant.
// See [getKeyAtDirection()] for the meaning. The starting point on the // See [getKeyAtDirection()] for the meaning. The starting point on the
// circle is the top direction. // circle is the top direction.
double a = Math.atan2(dy, dx) + Math.PI; double a = Math.atan2(dy, dx) + Math.PI;
// a is between 0 and 2pi, 0 is pointing to the left // a is between 0 and 2pi, 0 is pointing to the left
// add 12 to align 0 to the top // 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.gesture = new Gesture(direction);
{ KeyValue new_value = getNearestKeyAtDirection(ptr, direction);
ptr.selected_direction = direction; if (new_value != null)
KeyValue newValue = getNearestKeyAtDirection(ptr, direction); { // Pointer is swiping into a side key.
if (newValue != null && !newValue.equals(ptr.value))
{ ptr.value = new_value;
ptr.value = newValue; ptr.flags = pointer_flags_of_kv(new_value);
ptr.flags = pointer_flags_of_kv(newValue); // Sliding mode is entered when key5 or key6 is down on a slider key.
// Sliding mode is entered when key5 or key6 is down on a slider key. if (ptr.key.slider &&
if (ptr.key.slider && (new_value.equals(ptr.key.getKeyValue(5))
(newValue.equals(ptr.key.getKeyValue(5)) || new_value.equals(ptr.key.getKeyValue(6))))
|| newValue.equals(ptr.key.getKeyValue(6)))) {
{ startSliding(ptr, x);
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. */ /** A pointer is repeating. Returns [true] if repeat should continue. */
private boolean handleKeyRepeat(Pointer ptr) private boolean handleKeyRepeat(Pointer ptr)
{ {
@ -447,14 +478,51 @@ public final class Pointers implements Handler.Callback
return flags; 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 private static final class Pointer
{ {
/** -1 when latched. */ /** -1 when latched. */
public int pointerId; public int pointerId;
/** The Key pressed by this Pointer */ /** The Key pressed by this Pointer */
public final KeyboardData.Key key; public final KeyboardData.Key key;
/** Current direction. [null] means not swiping. */ /** Gesture state, see [Gesture]. [null] means the pointer has not moved out of the center region. */
public Integer selected_direction; public Gesture gesture;
/** Selected value with [modifiers] applied. */ /** Selected value with [modifiers] applied. */
public KeyValue value; public KeyValue value;
public float downX; public float downX;
@ -472,7 +540,7 @@ public final class Pointers implements Handler.Callback
{ {
pointerId = p; pointerId = p;
key = k; key = k;
selected_direction = null; gesture = null;
value = v; value = v;
downX = x; downX = x;
downY = y; downY = y;
@ -602,6 +670,14 @@ public final class Pointers implements Handler.Callback
return false; 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]. */ /** Returns the activated modifiers that are not in [m2]. */
public Iterator<KeyValue> diff(Modifiers m2) public Iterator<KeyValue> diff(Modifiers m2)
{ {