forked from extern/Unexpected-Keyboard
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:
parent
96fc4003f1
commit
4906f8105f
141
srcs/juloo.keyboard2/Gesture.java
Normal file
141
srcs/juloo.keyboard2/Gesture.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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<Pointers.Modifiers, KeyValue> cacheEntry(KeyValue k)
|
||||
{
|
||||
|
@ -27,6 +27,7 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
public static enum Modifier
|
||||
{
|
||||
SHIFT,
|
||||
GESTURE,
|
||||
CTRL,
|
||||
ALT,
|
||||
META,
|
||||
@ -54,8 +55,8 @@ public final class KeyValue implements Comparable<KeyValue>
|
||||
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<KeyValue>
|
||||
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)
|
||||
|
@ -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<KeyValue> diff(Modifiers m2)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user