submodule vendor/libswipetype now points to http://10.0.1.49:3010/harley/libswipetype.git (commit e4aef2f) instead of upstream github.com/libswipetype/libswipetype. Key change: removed [0,1] bounding-box normalization from both PathProcessor and IdealPathGenerator. Paths now stay in original dp coordinates so DTW measures actual keyboard distance between keys.
1021 lines
30 KiB
Java
1021 lines
30 KiB
Java
package juloo.keyboard2;
|
|
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
import java.util.NoSuchElementException;
|
|
|
|
/**
|
|
* Manage pointers (fingers) on the screen and long presses.
|
|
* Call back to IPointerEventHandler.
|
|
*/
|
|
public final class Pointers implements Handler.Callback
|
|
{
|
|
public static final int FLAG_P_LATCHABLE = 1;
|
|
public static final int FLAG_P_LATCHED = (1 << 1);
|
|
public static final int FLAG_P_FAKE = (1 << 2);
|
|
public static final int FLAG_P_DOUBLE_TAP_LOCK = (1 << 3);
|
|
public static final int FLAG_P_LOCKED = (1 << 4);
|
|
public static final int FLAG_P_SLIDING = (1 << 5);
|
|
/** Clear latched (only if also FLAG_P_LATCHABLE set). */
|
|
public static final int FLAG_P_CLEAR_LATCHED = (1 << 6);
|
|
/** Can't be locked, even when long pressing. */
|
|
public static final int FLAG_P_CANT_LOCK = (1 << 7);
|
|
|
|
private Handler _longpress_handler;
|
|
private ArrayList<Pointer> _ptrs = new ArrayList<Pointer>();
|
|
private IPointerEventHandler _handler;
|
|
private Config _config;
|
|
private UnexpectedKeyboardSwipeAdapter _swipeAdapter;
|
|
|
|
public Pointers(IPointerEventHandler h, Config c)
|
|
{
|
|
_longpress_handler = new Handler(this);
|
|
_handler = h;
|
|
_config = c;
|
|
}
|
|
|
|
public void setSwipeAdapter(UnexpectedKeyboardSwipeAdapter a) { _swipeAdapter = a; }
|
|
|
|
/** Return the list of modifiers currently activated. */
|
|
public Modifiers getModifiers()
|
|
{
|
|
return getModifiers(false);
|
|
}
|
|
|
|
/** When [skip_latched] is true, don't take flags of latched keys into account. */
|
|
private Modifiers getModifiers(boolean skip_latched)
|
|
{
|
|
int n_ptrs = _ptrs.size();
|
|
KeyValue[] mods = new KeyValue[n_ptrs];
|
|
int n_mods = 0;
|
|
for (int i = 0; i < n_ptrs; i++)
|
|
{
|
|
Pointer p = _ptrs.get(i);
|
|
if (p.value != null
|
|
&& !(skip_latched && p.hasFlagsAny(FLAG_P_LATCHED)
|
|
&& (p.flags & FLAG_P_LOCKED) == 0))
|
|
mods[n_mods++] = p.value;
|
|
}
|
|
return Modifiers.ofArray(mods, n_mods);
|
|
}
|
|
|
|
public void clear()
|
|
{
|
|
for (Pointer p : _ptrs)
|
|
stopLongPress(p);
|
|
_ptrs.clear();
|
|
}
|
|
|
|
public boolean isKeyDown(KeyboardData.Key k)
|
|
{
|
|
for (Pointer p : _ptrs)
|
|
if (p.key == k)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/** See [FLAG_P_*] flags. Returns [-1] if the key is not pressed. */
|
|
public int getKeyFlags(KeyValue kv)
|
|
{
|
|
for (Pointer p : _ptrs)
|
|
if (p.value != null && p.value.equals(kv))
|
|
return p.flags;
|
|
return -1;
|
|
}
|
|
|
|
/** The key must not be already latched . */
|
|
void add_fake_pointer(KeyboardData.Key key, KeyValue kv, boolean locked)
|
|
{
|
|
int flags = pointer_flags_of_kv(kv) | FLAG_P_FAKE | FLAG_P_LATCHED;
|
|
if (locked)
|
|
flags |= FLAG_P_LOCKED;
|
|
Pointer ptr = new Pointer(-1, key, kv, 0.f, 0.f, Modifiers.EMPTY, flags);
|
|
_ptrs.add(ptr);
|
|
_handler.onPointerFlagsChanged(false);
|
|
}
|
|
|
|
/** Set whether a key is latched or locked by adding a "fake" pointer, a
|
|
pointer that is not due to user interaction.
|
|
This is used by auto-capitalisation.
|
|
|
|
When [lock] is true, [latched] control whether the modifier is locked or disabled.
|
|
When [lock] is false, an existing locked pointer is not affected. */
|
|
public void set_fake_pointer_state(KeyboardData.Key key, KeyValue kv,
|
|
boolean latched, boolean lock)
|
|
{
|
|
Pointer ptr = getLatched(key, kv);
|
|
if (ptr == null)
|
|
{
|
|
// No existing pointer, latch the key.
|
|
if (latched)
|
|
{
|
|
add_fake_pointer(key, kv, lock);
|
|
_handler.onPointerFlagsChanged(false);
|
|
}
|
|
}
|
|
else if ((ptr.flags & FLAG_P_FAKE) == 0)
|
|
{} // Key already latched but not by a fake ptr, do nothing.
|
|
else if (lock)
|
|
{
|
|
// Acting on locked modifiers, replace the pointer each time.
|
|
removePtr(ptr);
|
|
if (latched)
|
|
add_fake_pointer(key, kv, lock);
|
|
_handler.onPointerFlagsChanged(false);
|
|
}
|
|
else if ((ptr.flags & FLAG_P_LOCKED) != 0)
|
|
{} // Existing ptr is locked but [lock] is false, do not continue.
|
|
else if (!latched)
|
|
{
|
|
// Key is latched by a fake ptr. Unlatch if requested.
|
|
removePtr(ptr);
|
|
_handler.onPointerFlagsChanged(false);
|
|
}
|
|
}
|
|
|
|
// Receiving events
|
|
|
|
public void onTouchUp(int pointerId)
|
|
{
|
|
Pointer ptr = getPtr(pointerId);
|
|
if (ptr == null)
|
|
return;
|
|
if (ptr.hasFlagsAny(FLAG_P_SLIDING))
|
|
{
|
|
clearLatched();
|
|
if (ptr.joypad != null)
|
|
ptr.joypad.onTouchUp(ptr);
|
|
else
|
|
ptr.sliding.onTouchUp(ptr);
|
|
return;
|
|
}
|
|
stopLongPress(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
|
|
{
|
|
removePtr(ptr); // Remove dupplicate
|
|
// Toggle lockable key, except if it's a fake pointer
|
|
if ((latched.flags & (FLAG_P_FAKE | FLAG_P_DOUBLE_TAP_LOCK)) == FLAG_P_DOUBLE_TAP_LOCK)
|
|
lockPointer(latched, false);
|
|
else // Otherwise, unlatch
|
|
{
|
|
removePtr(latched);
|
|
_handler.onPointerUp(ptr_value, ptr.modifiers);
|
|
}
|
|
}
|
|
else if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
|
|
{
|
|
// Latchable but non-special keys must clear latched.
|
|
if ((ptr.flags & FLAG_P_CLEAR_LATCHED) != 0)
|
|
clearLatched();
|
|
ptr.flags |= FLAG_P_LATCHED;
|
|
ptr.pointerId = -1;
|
|
_handler.onPointerFlagsChanged(false);
|
|
}
|
|
else
|
|
{
|
|
clearLatched();
|
|
removePtr(ptr);
|
|
_handler.onPointerUp(ptr_value, ptr.modifiers);
|
|
}
|
|
}
|
|
|
|
public void onTouchCancel()
|
|
{
|
|
clear();
|
|
_handler.onPointerFlagsChanged(true);
|
|
}
|
|
|
|
/** Cancel a single pointer by its ID — stops long-press and removes it. */
|
|
public void cancelPointer(int pointerId)
|
|
{
|
|
for (int i = _ptrs.size() - 1; i >= 0; i--)
|
|
{
|
|
Pointer ptr = _ptrs.get(i);
|
|
if (ptr.pointerId == pointerId && !ptr.hasFlagsAny(FLAG_P_LATCHED))
|
|
{
|
|
_longpress_handler.removeMessages(ptr.timeoutWhat);
|
|
_ptrs.remove(i);
|
|
break;
|
|
}
|
|
}
|
|
_handler.onPointerFlagsChanged(false);
|
|
}
|
|
|
|
/* Whether an other pointer is down on a non-special key. */
|
|
private boolean isOtherPointerDown()
|
|
{
|
|
for (Pointer p : _ptrs)
|
|
if (!p.hasFlagsAny(FLAG_P_LATCHED) &&
|
|
(p.value == null || !p.value.hasFlagsAny(KeyValue.FLAG_SPECIAL)))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
public void onTouchDown(float x, float y, int pointerId, KeyboardData.Key key)
|
|
{
|
|
// Ignore new presses while a sliding key is active. On some devices, ghost
|
|
// touch events can happen while the pointer travels on top of other keys.
|
|
if (isSliding())
|
|
return;
|
|
// Don't take latched modifiers into account if an other key is pressed.
|
|
// The other key already "own" the latched modifiers and will clear them.
|
|
Modifiers mods = getModifiers(isOtherPointerDown());
|
|
KeyValue value = _handler.modifyKey(key.keys[0], mods);
|
|
Pointer ptr = make_pointer(pointerId, key, value, x, y, mods);
|
|
_ptrs.add(ptr);
|
|
// Space bar gets a slightly shorter long-press (500ms) so the joypad
|
|
// activates quickly when holding space
|
|
if (value != null && value.getKind() == KeyValue.Kind.Editing
|
|
&& value.getEditing() == KeyValue.Editing.SPACE_BAR)
|
|
startLongPress(ptr, 500);
|
|
else
|
|
startLongPress(ptr);
|
|
_handler.onPointerDown(value, false);
|
|
}
|
|
|
|
static final int[] DIRECTION_TO_INDEX = new int[]{
|
|
7, 2, 2, 6, 6, 4, 4, 8, 8, 3, 3, 5, 5, 1, 1, 7
|
|
};
|
|
|
|
/**
|
|
* [direction] is an int between [0] and [15] that represent 16 sections of a
|
|
* circle, clockwise, starting at the top.
|
|
*/
|
|
static KeyValue getKeyAtDirection(KeyboardData.Key k, int direction)
|
|
{
|
|
return k.keys[DIRECTION_TO_INDEX[direction]];
|
|
}
|
|
|
|
/**
|
|
* 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, int direction)
|
|
{
|
|
KeyValue k;
|
|
// [i] is [0, -1, +1, ..., -3, +3], scanning 43% of the circle's area,
|
|
// centered on the initial swipe direction.
|
|
for (int i = 0; i > -4; i = (~i>>31) - i)
|
|
{
|
|
int d = (direction + i + 16) % 16;
|
|
// Don't make the difference between a key that doesn't exist and a key
|
|
// that is removed by [_handler]. Triggers side effects.
|
|
k = _handler.modifyKey(getKeyAtDirection(ptr.key, d), ptr.modifiers);
|
|
if (k != null)
|
|
{
|
|
// When the nearest key is a slider, it is only selected if it's placed
|
|
// within 18% of the original swipe direction.
|
|
// This reduces accidental swipes on the slider and allow typing circle
|
|
// gestures without being interrupted by the slider.
|
|
if (k.getKind() == KeyValue.Kind.Slider && Math.abs(i) >= 2)
|
|
continue;
|
|
return k;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void onTouchMove(float x, float y, int pointerId)
|
|
{
|
|
Pointer ptr = getPtr(pointerId);
|
|
if (ptr == null)
|
|
return;
|
|
if (ptr.hasFlagsAny(FLAG_P_SLIDING))
|
|
{
|
|
if (ptr.joypad != null)
|
|
{
|
|
ptr.joypad.onTouchMove(ptr, x, y);
|
|
}
|
|
else
|
|
{
|
|
ptr.sliding.onTouchMove(ptr, x, y);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// The position in a IME windows is clampled to view.
|
|
// For a better up swipe behaviour, set the y position to a negative value when clamped.
|
|
if (y == 0.0) y = -400;
|
|
float dx = x - ptr.downX;
|
|
float dy = y - ptr.downY;
|
|
|
|
float dist = Math.abs(dx) + Math.abs(dy);
|
|
if (dist < _config.swipe_dist_px)
|
|
{
|
|
// 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
|
|
int direction = ((int)(a * 8 / Math.PI) + 12) % 16;
|
|
if (ptr.gesture == null)
|
|
{ // Gesture starts
|
|
|
|
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);
|
|
// Start sliding mode
|
|
if (new_value.getKind() == KeyValue.Kind.Slider)
|
|
startSliding(ptr, x, y, dx, dy, new_value);
|
|
_handler.onPointerDown(new_value, true);
|
|
}
|
|
else if (ptr.value != null
|
|
&& ptr.value.getKind() == KeyValue.Kind.Slider
|
|
&& ptr.value.getSlider().isJoystick())
|
|
{
|
|
// Joystick: swipe in any direction activates it
|
|
startSliding(ptr, x, y, dx, dy, ptr.value);
|
|
}
|
|
|
|
}
|
|
else if (ptr.gesture.changed_direction(direction))
|
|
{ // Gesture changed state
|
|
if (!ptr.gesture.is_in_progress())
|
|
{ // Gesture ended
|
|
_handler.onPointerFlagsChanged(true);
|
|
}
|
|
else
|
|
{
|
|
ptr.value = apply_gesture(ptr, ptr.gesture.get_gesture());
|
|
restartLongPress(ptr);
|
|
ptr.flags = 0; // Special behaviors are ignored during a gesture.
|
|
_handler.onPointerFlagsChanged(true); // Vibrate
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pointers management
|
|
|
|
private Pointer getPtr(int pointerId)
|
|
{
|
|
for (Pointer p : _ptrs)
|
|
if (p.pointerId == pointerId)
|
|
return p;
|
|
return null;
|
|
}
|
|
|
|
private void removePtr(Pointer ptr)
|
|
{
|
|
_ptrs.remove(ptr);
|
|
}
|
|
|
|
private Pointer getLatched(Pointer target)
|
|
{
|
|
return getLatched(target.key, target.value);
|
|
}
|
|
|
|
private Pointer getLatched(KeyboardData.Key k, KeyValue v)
|
|
{
|
|
if (v == null)
|
|
return null;
|
|
for (Pointer p : _ptrs)
|
|
if (p.key == k && p.hasFlagsAny(FLAG_P_LATCHED)
|
|
&& p.value != null && p.value.equals(v))
|
|
return p;
|
|
return null;
|
|
}
|
|
|
|
private void clearLatched()
|
|
{
|
|
for (int i = _ptrs.size() - 1; i >= 0; i--)
|
|
{
|
|
Pointer ptr = _ptrs.get(i);
|
|
// Latched and not locked, remove
|
|
if (ptr.hasFlagsAny(FLAG_P_LATCHED) && (ptr.flags & FLAG_P_LOCKED) == 0)
|
|
_ptrs.remove(i);
|
|
// Not latched but pressed, don't latch once released and stop long press.
|
|
else if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
|
|
ptr.flags &= ~FLAG_P_LATCHABLE;
|
|
}
|
|
}
|
|
|
|
/** Make a pointer into the locked state. */
|
|
private void lockPointer(Pointer ptr, boolean shouldVibrate)
|
|
{
|
|
ptr.flags = (ptr.flags & ~FLAG_P_DOUBLE_TAP_LOCK) | FLAG_P_LOCKED;
|
|
_handler.onPointerFlagsChanged(shouldVibrate);
|
|
}
|
|
|
|
boolean isSliding()
|
|
{
|
|
for (Pointer ptr : _ptrs)
|
|
if (ptr.hasFlagsAny(FLAG_P_SLIDING))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// Key repeat
|
|
|
|
/** Message from [_longpress_handler]. */
|
|
@Override
|
|
public boolean handleMessage(Message msg)
|
|
{
|
|
for (Pointer ptr : _ptrs)
|
|
{
|
|
if (ptr.timeoutWhat == msg.what)
|
|
{
|
|
// Joypad auto-repeat takes priority over long-press/hold
|
|
if (ptr.joypad != null && ptr.hasFlagsAny(FLAG_P_SLIDING))
|
|
{
|
|
ptr.joypad.doRepeat(ptr);
|
|
}
|
|
else
|
|
{
|
|
handleLongPress(ptr);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static int uniqueTimeoutWhat = 0;
|
|
|
|
private void startLongPress(Pointer ptr)
|
|
{
|
|
int what = (uniqueTimeoutWhat++);
|
|
ptr.timeoutWhat = what;
|
|
_longpress_handler.sendEmptyMessageDelayed(what, _config.longPressTimeout);
|
|
}
|
|
|
|
/** Start long-press timer with a custom timeout override (ms). */
|
|
private void startLongPress(Pointer ptr, long timeoutMs)
|
|
{
|
|
int what = (uniqueTimeoutWhat++);
|
|
ptr.timeoutWhat = what;
|
|
_longpress_handler.sendEmptyMessageDelayed(what, timeoutMs);
|
|
}
|
|
|
|
private void stopLongPress(Pointer ptr)
|
|
{
|
|
_longpress_handler.removeMessages(ptr.timeoutWhat);
|
|
}
|
|
|
|
private void restartLongPress(Pointer ptr)
|
|
{
|
|
stopLongPress(ptr);
|
|
startLongPress(ptr);
|
|
}
|
|
|
|
/** A pointer is long pressing. */
|
|
private void handleLongPress(Pointer ptr)
|
|
{
|
|
// Long press toggle lock on modifiers
|
|
if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
|
|
{
|
|
if (!ptr.hasFlagsAny(FLAG_P_CANT_LOCK))
|
|
lockPointer(ptr, true);
|
|
return;
|
|
}
|
|
// Latched key, no key
|
|
if (ptr.hasFlagsAny(FLAG_P_LATCHED) || ptr.value == null)
|
|
return;
|
|
// Key is long-pressable
|
|
KeyValue kv = KeyModifier.modify_long_press(ptr.value);
|
|
if (!kv.equals(ptr.value))
|
|
{
|
|
ptr.value = kv;
|
|
_handler.onPointerDown(kv, true);
|
|
return;
|
|
}
|
|
// Space bar long-press: enter joypad cursor mode
|
|
if (ptr.value.getKind() == KeyValue.Kind.Editing
|
|
&& ptr.value.getEditing() == KeyValue.Editing.SPACE_BAR)
|
|
{
|
|
stopLongPress(ptr);
|
|
ptr.flags |= FLAG_P_SLIDING;
|
|
ptr.joypad = new Joypad(ptr.downX, ptr.downY,
|
|
KeyValue.Slider.Cursor_joystick);
|
|
return;
|
|
}
|
|
// Special keys
|
|
if (kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
|
|
return;
|
|
// For every other keys, key-repeat
|
|
if (_config.keyrepeat_enabled)
|
|
{
|
|
_handler.onPointerHold(kv, ptr.modifiers);
|
|
_longpress_handler.sendEmptyMessageDelayed(ptr.timeoutWhat,
|
|
_config.longPressInterval);
|
|
}
|
|
}
|
|
|
|
// Sliding
|
|
|
|
/** When sliding is ongoing, key events are handled by the [Slider] class.
|
|
[kv] must be of kind [Slider]. */
|
|
void startSliding(Pointer ptr, float x, float y, float dx, float dy, KeyValue kv)
|
|
{
|
|
int r = kv.getSliderRepeat();
|
|
int dirx = dx < 0 ? -r : r;
|
|
int diry = dy < 0 ? -r : r;
|
|
stopLongPress(ptr);
|
|
ptr.flags |= FLAG_P_SLIDING;
|
|
KeyValue.Slider s = kv.getSlider();
|
|
if (s.isJoystick())
|
|
{
|
|
ptr.joypad = new Joypad(x, y, s);
|
|
}
|
|
else
|
|
{
|
|
ptr.sliding = new Sliding(x, y, dirx, diry, s);
|
|
}
|
|
}
|
|
|
|
/** Return the [FLAG_P_*] flags that correspond to pressing [kv]. */
|
|
int pointer_flags_of_kv(KeyValue kv)
|
|
{
|
|
int flags = 0;
|
|
if (kv.hasFlagsAny(KeyValue.FLAG_LATCH))
|
|
{
|
|
// Non-special latchable key must clear modifiers and can't be locked
|
|
if (!kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
|
|
flags |= FLAG_P_CLEAR_LATCHED | FLAG_P_CANT_LOCK;
|
|
flags |= FLAG_P_LATCHABLE;
|
|
}
|
|
if (_config.double_tap_lock_shift &&
|
|
kv.hasFlagsAny(KeyValue.FLAG_DOUBLE_TAP_LOCK))
|
|
flags |= FLAG_P_DOUBLE_TAP_LOCK;
|
|
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.anticircle, 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
|
|
|
|
Pointer make_pointer(int p, KeyboardData.Key k, KeyValue v, float x, float y,
|
|
Modifiers m)
|
|
{
|
|
int flags = (v == null) ? 0 : pointer_flags_of_kv(v);
|
|
return new Pointer(p, k, v, x, y, m, flags);
|
|
}
|
|
|
|
private static final class Pointer
|
|
{
|
|
/** -1 when latched. */
|
|
public int pointerId;
|
|
/** The Key pressed by this Pointer */
|
|
public final KeyboardData.Key key;
|
|
/** 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;
|
|
public float downY;
|
|
/** Modifier flags at the time the key was pressed. */
|
|
public Modifiers modifiers;
|
|
/** See [FLAG_P_*] flags. */
|
|
public int flags;
|
|
/** Identify timeout messages. */
|
|
public int timeoutWhat;
|
|
/** [null] when not in sliding mode. */
|
|
public Sliding sliding;
|
|
/** [null] when not in joypad mode. */
|
|
public Joypad joypad;
|
|
|
|
public Pointer(int p, KeyboardData.Key k, KeyValue v, float x, float y, Modifiers m, int f)
|
|
{
|
|
pointerId = p;
|
|
key = k;
|
|
gesture = null;
|
|
value = v;
|
|
downX = x;
|
|
downY = y;
|
|
modifiers = m;
|
|
flags = f;
|
|
timeoutWhat = -1;
|
|
sliding = null;
|
|
joypad = null;
|
|
}
|
|
|
|
public boolean hasFlagsAny(int has)
|
|
{
|
|
return ((flags & has) != 0);
|
|
}
|
|
}
|
|
|
|
public final class Sliding
|
|
{
|
|
/** Accumulated distance since last event. */
|
|
float d = 0.f;
|
|
/** The slider speed changes depending on the pointer speed. */
|
|
float speed = 0.5f;
|
|
/** Coordinate of the last move. */
|
|
float last_x;
|
|
float last_y;
|
|
/** [System.currentTimeMillis()] at the time of the last move. Equals to
|
|
[-1] when the sliding hasn't started yet. */
|
|
long last_move_ms = -1;
|
|
/** The property which is being slided. */
|
|
KeyValue.Slider slider;
|
|
/** Direction of the initial movement, positive if sliding to the right and
|
|
negative if sliding to the left. */
|
|
int direction_x;
|
|
int direction_y;
|
|
|
|
public Sliding(float x, float y, int dirx, int diry, KeyValue.Slider s)
|
|
{
|
|
last_x = x;
|
|
last_y = y;
|
|
slider = s;
|
|
direction_x = dirx;
|
|
direction_y = diry;
|
|
}
|
|
|
|
static final float SPEED_SMOOTHING = 0.7f;
|
|
/** Avoid absurdly large values. */
|
|
static final float SPEED_MAX = 4.f;
|
|
/** Make vertical sliders slower. The intention is to make the up/down
|
|
slider slower, as we have less visibility and do smaller movements in
|
|
that direction. */
|
|
static final float SPEED_VERTICAL_MULT = 0.5f;
|
|
/** Make horizontal sliders slower while ctrl is held (which typically
|
|
means movement happens by whole words instead of characters) */
|
|
static final float SPEED_WORD_MULT = 0.25f;
|
|
|
|
public void onTouchMove(Pointer ptr, float x, float y)
|
|
{
|
|
// Start sliding only after the pointer has travelled an other distance.
|
|
// This allows to trigger the slider movements only once with a short
|
|
// swipe.
|
|
float travelled = Math.abs(x - last_x) + Math.abs(y - last_y);
|
|
if (last_move_ms == -1)
|
|
{
|
|
if (travelled < (_config.swipe_dist_px + _config.slide_step_px))
|
|
return;
|
|
last_move_ms = System.currentTimeMillis();
|
|
}
|
|
float current_speed = speed / _config.slide_step_px;
|
|
if (slider.isVertical()) {
|
|
d += (y - last_y) * current_speed * direction_y * SPEED_VERTICAL_MULT;
|
|
} else {
|
|
if (ptr.modifiers.has(KeyValue.Modifier.CTRL))
|
|
current_speed *= SPEED_WORD_MULT;
|
|
d += (x - last_x) * current_speed * direction_x;
|
|
}
|
|
|
|
update_speed(travelled, x, y);
|
|
// Send an event when [abs(d)] exceeds [1].
|
|
int d_ = (int)d;
|
|
if (d_ != 0)
|
|
{
|
|
d -= d_;
|
|
_handler.onPointerHold(KeyValue.sliderKey(slider, d_),
|
|
ptr.modifiers);
|
|
}
|
|
}
|
|
|
|
/** Handle a sliding pointer going up. Latched modifiers are not
|
|
cleared to allow easy adjustments to the cursors. The pointer is
|
|
cancelled. */
|
|
public void onTouchUp(Pointer ptr)
|
|
{
|
|
removePtr(ptr);
|
|
_handler.onPointerFlagsChanged(false);
|
|
}
|
|
|
|
/** [speed] is computed from the elapsed time and distance traveled
|
|
between two move events. Exponential smoothing is used to smooth out
|
|
the noise. Sets [last_move_ms] and [last_pos]. */
|
|
void update_speed(float travelled, float x, float y)
|
|
{
|
|
long now = System.currentTimeMillis();
|
|
float instant_speed = Math.min(SPEED_MAX,
|
|
travelled / (float)(now - last_move_ms) + 1.f);
|
|
speed = speed + (instant_speed - speed) * SPEED_SMOOTHING;
|
|
last_move_ms = now;
|
|
last_x = x;
|
|
last_y = y;
|
|
}
|
|
}
|
|
|
|
/** Joypad: direction-lock + auto-repeat cursor control with speed curve.
|
|
Designed for the space bar (limited room for finger travel).
|
|
|
|
Once the finger moves past the dead zone, the dominant direction is
|
|
locked and cursor events auto-repeat at a rate and step count that
|
|
increase with finger distance from origin (farther = faster). */
|
|
public final class Joypad
|
|
{
|
|
/** The last locked direction, or null if inactive. */
|
|
KeyValue.Slider _activeDir = null;
|
|
/** Position of the finger when joypad was activated (down point). */
|
|
float _originX;
|
|
float _originY;
|
|
/** The slider this joypad delegates to. */
|
|
KeyValue.Slider slider;
|
|
/** Current speed: steps per auto-repeat tick [1..5]. */
|
|
int _stepsPerTick = 1;
|
|
/** Current interval between ticks (ms). */
|
|
int _repeatMs;
|
|
|
|
public Joypad(float x, float y, KeyValue.Slider s)
|
|
{
|
|
_originX = x;
|
|
_originY = y;
|
|
slider = s;
|
|
_repeatMs = (int)_config.longPressInterval;
|
|
}
|
|
|
|
/** Pick the dominant direction from a finger offset deltas. */
|
|
KeyValue.Slider pickDirection(float dx, float dy)
|
|
{
|
|
if (Math.abs(dx) >= Math.abs(dy))
|
|
return (dx > 0) ? KeyValue.Slider.Cursor_right
|
|
: KeyValue.Slider.Cursor_left;
|
|
else
|
|
return (dy > 0) ? KeyValue.Slider.Cursor_down
|
|
: KeyValue.Slider.Cursor_up;
|
|
}
|
|
|
|
/** Compute speed curve from finger displacement.
|
|
[dist] = Manhattan distance from origin.
|
|
Returns [stepsPerTick, repeatIntervalMs]. */
|
|
void computeSpeed(float dist)
|
|
{
|
|
float d = dist - _config.swipe_dist_px; // distance beyond dead zone
|
|
if (d < 1f) d = 1f;
|
|
float maxD = _config.swipe_dist_px * 6f; // ~6x dead zone = max speed
|
|
float t = Math.min(d / maxD, 1f); // 0..1
|
|
// Quadratic curve: gentle near center, faster at extremes
|
|
float curve = t * t;
|
|
_stepsPerTick = 1 + (int)(curve * 4f); // 1..5 steps/tick
|
|
int base = (int)_config.longPressInterval;
|
|
_repeatMs = Math.max(30, base - (int)((base - 30) * curve));
|
|
}
|
|
|
|
public void onTouchMove(Pointer ptr, float x, float y)
|
|
{
|
|
float dx = x - _originX;
|
|
float dy = y - _originY;
|
|
float dist = Math.abs(dx) + Math.abs(dy);
|
|
|
|
// Dead zone: finger hasn't moved far enough from origin
|
|
if (dist < _config.swipe_dist_px)
|
|
{
|
|
if (_activeDir != null)
|
|
{
|
|
// Returned to center → stop auto-repeat
|
|
_activeDir = null;
|
|
_longpress_handler.removeMessages(ptr.timeoutWhat);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Update speed curve from current distance
|
|
computeSpeed(dist);
|
|
|
|
KeyValue.Slider newDir = pickDirection(dx, dy);
|
|
|
|
if (newDir != _activeDir)
|
|
{
|
|
// Direction changed (or first lock) → fire and start/restart auto-repeat
|
|
_activeDir = newDir;
|
|
_longpress_handler.removeMessages(ptr.timeoutWhat);
|
|
_handler.onPointerHold(
|
|
KeyValue.sliderKey(newDir, _stepsPerTick), ptr.modifiers);
|
|
// Schedule next repeat with computed interval
|
|
_longpress_handler.sendEmptyMessageDelayed(
|
|
ptr.timeoutWhat, _repeatMs);
|
|
}
|
|
else
|
|
{
|
|
// Same direction: don't reschedule timer (touch events come faster
|
|
// than the timer, which would keep pushing it out). The pending
|
|
// doRepeat tick will pick up the updated _stepsPerTick / _repeatMs.
|
|
}
|
|
}
|
|
|
|
/** Called by Pointers.handleMessage for auto-repeat ticks. */
|
|
public void doRepeat(Pointer ptr)
|
|
{
|
|
if (_activeDir == null) return;
|
|
_handler.onPointerHold(
|
|
KeyValue.sliderKey(_activeDir, _stepsPerTick), ptr.modifiers);
|
|
_longpress_handler.sendEmptyMessageDelayed(
|
|
ptr.timeoutWhat, _repeatMs);
|
|
}
|
|
|
|
public void onTouchUp(Pointer ptr)
|
|
{
|
|
_activeDir = null;
|
|
_longpress_handler.removeMessages(ptr.timeoutWhat);
|
|
removePtr(ptr);
|
|
_handler.onPointerFlagsChanged(false);
|
|
}
|
|
}
|
|
|
|
/** Represent modifiers currently activated.
|
|
Sorted in the order they should be evaluated. */
|
|
public static final class Modifiers
|
|
{
|
|
private final KeyValue[] _mods;
|
|
private final int _size;
|
|
|
|
private Modifiers(KeyValue[] m, int s)
|
|
{
|
|
_mods = m; _size = s;
|
|
}
|
|
|
|
public KeyValue get(int i) { return _mods[_size - 1 - i]; }
|
|
public int size() { return _size; }
|
|
public boolean has(KeyValue.Modifier m)
|
|
{
|
|
for (int i = 0; i < _size; i++)
|
|
{
|
|
KeyValue kv = _mods[i];
|
|
switch (kv.getKind())
|
|
{
|
|
case Modifier:
|
|
if (kv.getModifier().equals(m))
|
|
return true;
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
return new ModifiersDiffIterator(this, m2);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() { return Arrays.hashCode(_mods); }
|
|
@Override
|
|
public boolean equals(Object obj)
|
|
{
|
|
return Arrays.equals(_mods, ((Modifiers)obj)._mods);
|
|
}
|
|
|
|
public static final Modifiers EMPTY =
|
|
new Modifiers(new KeyValue[0], 0);
|
|
|
|
protected static Modifiers ofArray(KeyValue[] mods, int size)
|
|
{
|
|
// Sort and remove duplicates and nulls.
|
|
if (size > 1)
|
|
{
|
|
Arrays.sort(mods, 0, size);
|
|
int j = 0;
|
|
for (int i = 0; i < size; i++)
|
|
{
|
|
KeyValue m = mods[i];
|
|
if (m != null && (i + 1 >= size || m != mods[i + 1]))
|
|
{
|
|
mods[j] = m;
|
|
j++;
|
|
}
|
|
}
|
|
size = j;
|
|
}
|
|
return new Modifiers(mods, size);
|
|
}
|
|
|
|
/** Returns modifiers that are in [m1_] but not in [m2_]. */
|
|
static final class ModifiersDiffIterator
|
|
implements Iterator<KeyValue>
|
|
{
|
|
Modifiers m1;
|
|
int i1 = 0;
|
|
Modifiers m2;
|
|
int i2 = 0;
|
|
|
|
public ModifiersDiffIterator(Modifiers m1_, Modifiers m2_)
|
|
{
|
|
m1 = m1_;
|
|
m2 = m2_;
|
|
advance();
|
|
}
|
|
|
|
public boolean hasNext()
|
|
{
|
|
return i1 < m1._size;
|
|
}
|
|
|
|
public KeyValue next()
|
|
{
|
|
if (i1 >= m1._size)
|
|
throw new NoSuchElementException();
|
|
KeyValue m = m1._mods[i1];
|
|
i1++;
|
|
advance();
|
|
return m;
|
|
}
|
|
|
|
/** Advance to the next element if [i1] is not a valid element. The end
|
|
is reached when [i1 = m1.size()]. */
|
|
void advance()
|
|
{
|
|
while (i1 < m1.size())
|
|
{
|
|
KeyValue m = m1._mods[i1];
|
|
while (true)
|
|
{
|
|
if (i2 >= m2._size)
|
|
return;
|
|
int d = m.compareTo(m2._mods[i2]);
|
|
if (d < 0)
|
|
return;
|
|
i2++;
|
|
if (d == 0)
|
|
break;
|
|
}
|
|
i1++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public interface IPointerEventHandler
|
|
{
|
|
/** Key can be modified or removed by returning [null]. */
|
|
public KeyValue modifyKey(KeyValue k, Modifiers mods);
|
|
|
|
/** A key is pressed. [getModifiers()] is uptodate. Might be called after a
|
|
press or a swipe to a different value. Down events are not paired with
|
|
up events. */
|
|
public void onPointerDown(KeyValue k, boolean isSwipe);
|
|
|
|
/** Key is released. [k] is the key that was returned by
|
|
[modifySelectedKey] or [modifySelectedKey]. */
|
|
public void onPointerUp(KeyValue k, Modifiers mods);
|
|
|
|
/** Flags changed because latched or locked keys or cancelled pointers. */
|
|
public void onPointerFlagsChanged(boolean shouldVibrate);
|
|
|
|
/** Key is repeating. */
|
|
public void onPointerHold(KeyValue k, Modifiers mods);
|
|
}
|
|
}
|