Allow 8 symbols per key

'Keyboard.Key' now contains an array of size 9, giving each keyvalue an
index. The algorithm for finding the nearest key during a swipe now
needs 16 segments, which are now calculated as an angle.

The algorithm does one more interation instead of 2 more, slightly
reducing the sensitivity of corner values. The 'getAtDirection' function
is moved into the Pointers class to clearly separate the two systems.

The 'edgekey' attribute is now obsolete but is kept for compatibility.
The flag is removed internally, key index are simply translated.
Similarly, the 'slider' attribute now act on keys at index 5 and 6
instead of 2 and 3.
This commit is contained in:
Jules Aguillon 2023-03-03 19:44:05 +01:00
parent 1f9e92ed60
commit a6fe5cae00
5 changed files with 168 additions and 166 deletions

View File

@ -1,4 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- This file defines the QWERTY layout.
A layout is made of keys arranged into rows. Keys can be made bigger with the
'width' attribute and blank space can be added on the left of a key with the
'shift' attribute.
'key0' assigns the symbol on the middle of the key. 'key1', 'key2', etc..
assign symbols to the corners of a key, they are arranged like this:
1 7 2
5 0 6
3 8 4
Keys prefixed with 'loc ' are not visible on the keyboard. They are used to
specify a place for a key, if it needed to be added to the layout later.
(for example, by the "Add keys to keyboard" option)
See bottom_row.xml for the definition of the bottom row and neo2.xml for a
layout that re-defines it.
See srcs/juloo.keyboard2/KeyValue.java for the keys that have a special meaning.
-->
<keyboard> <keyboard>
<row> <row>
<key key0="q" key2="1" key4="esc"/> <key key0="q" key2="1" key4="esc"/>

View File

@ -202,7 +202,14 @@ final class KeyValue
@Override @Override
public boolean equals(Object obj) public boolean equals(Object obj)
{ {
KeyValue snd = (KeyValue)obj; return sameKey((KeyValue)obj);
}
/** Type-safe alternative to [equals]. */
public boolean sameKey(KeyValue snd)
{
if (snd == null)
return false;
return _symbol.equals(snd._symbol) && _code == snd._code; return _symbol.equals(snd._symbol) && _code == snd._code;
} }

View File

@ -260,6 +260,19 @@ public class Keyboard2View extends View
} }
} }
/** Horizontal and vertical position of the 9 indexes. */
static final Paint.Align[] LABEL_POSITION_H = new Paint.Align[]{
Paint.Align.CENTER, Paint.Align.LEFT, Paint.Align.RIGHT, Paint.Align.LEFT,
Paint.Align.RIGHT, Paint.Align.LEFT, Paint.Align.RIGHT,
Paint.Align.CENTER, Paint.Align.CENTER
};
static final Vertical[] LABEL_POSITION_V = new Vertical[]{
Vertical.CENTER, Vertical.TOP, Vertical.TOP, Vertical.BOTTOM,
Vertical.BOTTOM, Vertical.CENTER, Vertical.CENTER, Vertical.TOP,
Vertical.BOTTOM
};
@Override @Override
protected void onDraw(Canvas canvas) protected void onDraw(Canvas canvas)
{ {
@ -282,20 +295,12 @@ public class Keyboard2View extends View
float keyW = _keyWidth * k.width - _config.keyHorizontalInterval; float keyW = _keyWidth * k.width - _config.keyHorizontalInterval;
boolean isKeyDown = _pointers.isKeyDown(k); boolean isKeyDown = _pointers.isKeyDown(k);
drawKeyFrame(canvas, x, y, keyW, keyH, isKeyDown); drawKeyFrame(canvas, x, y, keyW, keyH, isKeyDown);
drawLabel(canvas, k.key0, keyW / 2f + x, y, keyH, isKeyDown); if (k.keys[0] != null)
if (k.edgekeys) drawLabel(canvas, k.keys[0], keyW / 2f + x, y, keyH, isKeyDown);
for (int i = 1; i < 9; i++)
{ {
drawSubLabel(canvas, k.key1, x, y, keyW, keyH, Paint.Align.CENTER, Vertical.TOP, isKeyDown); if (k.keys[i] != null)
drawSubLabel(canvas, k.key3, x, y, keyW, keyH, Paint.Align.LEFT, Vertical.CENTER, isKeyDown); drawSubLabel(canvas, k.keys[i], x, y, keyW, keyH, i, isKeyDown);
drawSubLabel(canvas, k.key2, x, y, keyW, keyH, Paint.Align.RIGHT, Vertical.CENTER, isKeyDown);
drawSubLabel(canvas, k.key4, x, y, keyW, keyH, Paint.Align.CENTER, Vertical.BOTTOM, isKeyDown);
}
else
{
drawSubLabel(canvas, k.key1, x, y, keyW, keyH, Paint.Align.LEFT, Vertical.TOP, isKeyDown);
drawSubLabel(canvas, k.key3, x, y, keyW, keyH, Paint.Align.LEFT, Vertical.BOTTOM, isKeyDown);
drawSubLabel(canvas, k.key2, x, y, keyW, keyH, Paint.Align.RIGHT, Vertical.TOP, isKeyDown);
drawSubLabel(canvas, k.key4, x, y, keyW, keyH, Paint.Align.RIGHT, Vertical.BOTTOM, isKeyDown);
} }
if (k.indication != null) if (k.indication != null)
{ {
@ -367,8 +372,6 @@ public class Keyboard2View extends View
private void drawLabel(Canvas canvas, KeyboardData.Corner k, float x, float y, float keyH, boolean isKeyDown) private void drawLabel(Canvas canvas, KeyboardData.Corner k, float x, float y, float keyH, boolean isKeyDown)
{ {
if (k == null)
return;
KeyValue kv = KeyModifier.modify(k.kv, _mods); KeyValue kv = KeyModifier.modify(k.kv, _mods);
if (kv == null) if (kv == null)
return; return;
@ -381,11 +384,10 @@ public class Keyboard2View extends View
} }
private void drawSubLabel(Canvas canvas, KeyboardData.Corner k, float x, private void drawSubLabel(Canvas canvas, KeyboardData.Corner k, float x,
float y, float keyW, float keyH, Paint.Align a, Vertical v, float y, float keyW, float keyH, int sub_index, boolean isKeyDown)
boolean isKeyDown)
{ {
if (k == null) Paint.Align a = LABEL_POSITION_H[sub_index];
return; Vertical v = LABEL_POSITION_V[sub_index];
KeyValue kv = KeyModifier.modify(k.kv, _mods); KeyValue kv = KeyModifier.modify(k.kv, _mods);
if (kv == null) if (kv == null)
return; return;

View File

@ -5,6 +5,7 @@ import android.content.res.XmlResourceParser;
import android.util.Xml; import android.util.Xml;
import java.io.StringReader; import java.io.StringReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -278,50 +279,44 @@ class KeyboardData
public static class Key public static class Key
{ {
/* /**
** 1 2 * 1 7 2
** 0 * 5 0 6
** 3 4 * 3 8 4
*/ */
public final Corner key0; public final Corner[] keys;
public final Corner key1;
public final Corner key2;
public final Corner key3;
public final Corner key4;
/** Key width in relative unit. */ /** Key width in relative unit. */
public final float width; public final float width;
/** Extra empty space on the left of the key. */ /** Extra empty space on the left of the key. */
public final float shift; public final float shift;
/** Put keys 1 to 4 on the edges instead of the corners. */
public final boolean edgekeys;
/** Keys 2 and 3 are repeated as the finger moves laterally on the key. /** Keys 2 and 3 are repeated as the finger moves laterally on the key.
Used for the left and right arrow keys on the space bar. */ Used for the left and right arrow keys on the space bar. */
public final boolean slider; public final boolean slider;
/** String printed on the keys. It has no other effect. */ /** String printed on the keys. It has no other effect. */
public final String indication; public final String indication;
protected Key(Corner k0, Corner k1, Corner k2, Corner k3, Corner k4, float w, float s, boolean e, boolean sl, String i) protected Key(Corner[] ks, float w, float s, boolean sl, String i)
{ {
key0 = k0; keys = ks;
key1 = k1;
key2 = k2;
key3 = k3;
key4 = k4;
width = w; width = w;
shift = s; shift = s;
edgekeys = e;
slider = sl; slider = sl;
indication = i; indication = i;
} }
public static Key parse(XmlPullParser parser) throws Exception public static Key parse(XmlPullParser parser) throws Exception
{ {
Corner k0 = Corner.parse_of_attr(parser, "key0"); Corner[] ks = new Corner[9];
Corner k1 = Corner.parse_of_attr(parser, "key1"); ks[0] = Corner.parse_of_attr(parser, "key0");
Corner k2 = Corner.parse_of_attr(parser, "key2"); ks[1] = Corner.parse_of_attr(parser, "key1");
Corner k3 = Corner.parse_of_attr(parser, "key3"); ks[2] = Corner.parse_of_attr(parser, "key2");
Corner k4 = Corner.parse_of_attr(parser, "key4"); ks[3] = Corner.parse_of_attr(parser, "key3");
ks[4] = Corner.parse_of_attr(parser, "key4");
ks[5] = Corner.parse_of_attr(parser, "key5");
ks[6] = Corner.parse_of_attr(parser, "key6");
ks[7] = Corner.parse_of_attr(parser, "key7");
ks[8] = Corner.parse_of_attr(parser, "key8");
float width = attribute_float(parser, "width", 1f); float width = attribute_float(parser, "width", 1f);
float shift = attribute_float(parser, "shift", 0.f); float shift = attribute_float(parser, "shift", 0.f);
boolean edgekeys = attribute_bool(parser, "edgekeys", false); boolean edgekeys = attribute_bool(parser, "edgekeys", false);
@ -329,112 +324,86 @@ class KeyboardData
String indication = parser.getAttributeValue(null, "indication"); String indication = parser.getAttributeValue(null, "indication");
while (parser.next() != XmlPullParser.END_TAG) while (parser.next() != XmlPullParser.END_TAG)
continue ; continue ;
return new Key(k0, k1, k2, k3, k4, width, shift, edgekeys, slider, indication); if (edgekeys)
ks = rearange_edgekeys(ks);
return new Key(ks, width, shift, slider, indication);
} }
/** New key with the width multiplied by 's'. */ /** New key with the width multiplied by 's'. */
public Key scaleWidth(float s) public Key scaleWidth(float s)
{ {
return new Key(key0, key1, key2, key3, key4, width * s, shift, edgekeys, return new Key(keys, width * s, shift, slider, indication);
slider, indication);
} }
public void getKeys(Set<KeyValue> dst) public void getKeys(Set<KeyValue> dst)
{ {
getCorner(dst, key0); for (int i = 0; i < keys.length; i++)
getCorner(dst, key1); if (keys[i] != null)
getCorner(dst, key2); dst.add(keys[i].kv);
getCorner(dst, key3);
getCorner(dst, key4);
} }
void getCorner(Set<KeyValue> dst, Corner k) { if (k != null) dst.add(k.kv); }
public KeyValue getKeyValue(int i) public KeyValue getKeyValue(int i)
{ {
Corner c; if (keys[i] == null)
switch (i) return null;
{ return keys[i].kv;
case 0: c = key0; break;
case 1: c = key1; break;
case 2: c = key2; break;
case 3: c = key3; break;
case 4: c = key4; break;
default: c = null; break;
}
return (c == null) ? null : c.kv;
} }
public Key withKeyValue(int i, KeyValue kv) public Key withKeyValue(int i, KeyValue kv)
{ {
Corner k0 = key0, k1 = key1, k2 = key2, k3 = key3, k4 = key4; Corner[] ks = Arrays.copyOf(keys, keys.length);
Corner k = Corner.of_kv(kv); ks[i] = Corner.of_kv(kv);
switch (i) return new Key(ks, width, shift, slider, indication);
{
case 0: k0 = k; break;
case 1: k1 = k; break;
case 2: k2 = k; break;
case 3: k3 = k; break;
case 4: k4 = k; break;
}
return new Key(k0, k1, k2, k3, k4, width, shift, edgekeys, slider,
indication);
} }
public Key withShift(float s) public Key withShift(float s)
{ {
return new Key(key0, key1, key2, key3, key4, width, s, edgekeys, slider, return new Key(keys, width, s, slider, indication);
indication);
}
/**
* See Pointers.onTouchMove() for the represented direction.
*/
public KeyValue getAtDirection(int direction)
{
Corner c = null;
if (edgekeys)
{
// \ 1 /
// \ /
// 3 0 2
// / \
// / 4 \
switch (direction)
{
case 2: case 3: c = key1; break;
case 4: case 5: c = key2; break;
case 6: case 7: c = key4; break;
case 8: case 1: c = key3; break;
}
}
else
{
// 1 | 2
// |
// --0--
// |
// 3 | 4
switch (direction)
{
case 1: case 2: c = key1; break;
case 3: case 4: c = key2; break;
case 5: case 6: c = key4; break;
case 7: case 8: c = key3; break;
}
}
return (c == null) ? null : c.kv;
} }
public boolean hasValue(KeyValue kv) public boolean hasValue(KeyValue kv)
{ {
return (hasValue(key0, kv) || hasValue(key1, kv) || hasValue(key2, kv) || for (int i = 0; i < keys.length; i++)
hasValue(key3, kv) || hasValue(key4, kv)); if (keys[i] != null && keys[i].kv.equals(kv))
return true;
return false;
} }
private static boolean hasValue(Corner c, KeyValue kv) public boolean hasValue(KeyValue kv, int i)
{ {
return (c != null && c.kv.equals(kv)); if (keys[i] == null)
return false;
return kv.equals(keys[i].kv);
}
/** Transform the key index for edgekeys.
* This option is no longer useful but is used by some layouts.
* 1 7 2 5 1 7
* 5 0 6 3 0 2
* 3 8 4 8 4 6
*/
static Corner[] rearange_edgekeys(Corner[] ks)
{
Corner[] edge_ks = new Corner[ks.length];
for (int i = 0; i < ks.length; i++)
edge_ks[edgekey_index(i)] = ks[i];
return edge_ks;
}
static int edgekey_index(int i)
{
switch (i)
{
case 5: return 1;
case 1: return 7;
case 7: return 2;
case 3: return 5;
case 2: return 6;
case 8: return 3;
case 4: return 8;
case 6: return 4;
default: return i;
}
} }
} }
@ -491,15 +460,15 @@ class KeyboardData
public Key apply(Key k) public Key apply(Key k)
{ {
return new Key(apply(k.key0), apply(k.key1), apply(k.key2), Corner[] ks = new Corner[k.keys.length];
apply(k.key3), apply(k.key4), k.width, k.shift, k.edgekeys, for (int i = 0; i < ks.length; i++)
k.slider, k.indication); if (k.keys[i] != null)
ks[i] = apply(k.keys[i]);
return new Key(ks, k.width, k.shift, k.slider, k.indication);
} }
protected Corner apply(Corner c) protected Corner apply(Corner c)
{ {
if (c == null)
return null;
KeyValue kv = apply(c.kv, c.localized); KeyValue kv = apply(c.kv, c.localized);
return (kv == null) ? null : new Corner(kv, c.localized); return (kv == null) ? null : new Corner(kv, c.localized);
} }

View File

@ -167,32 +167,49 @@ public final class Pointers implements Handler.Callback
// Don't take latched modifiers into account if an other key is pressed. // 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. // The other key already "own" the latched modifiers and will clear them.
Modifiers mods = getModifiers(isOtherPointerDown()); Modifiers mods = getModifiers(isOtherPointerDown());
KeyValue value = handleKV(key.key0, mods); KeyValue value = handleKV(key.keys[0], mods);
Pointer ptr = new Pointer(pointerId, key, value, x, y, mods); Pointer ptr = new Pointer(pointerId, key, value, x, y, mods);
_ptrs.add(ptr); _ptrs.add(ptr);
startKeyRepeat(ptr); startKeyRepeat(ptr);
_handler.onPointerDown(false); _handler.onPointerDown(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.
*/
KeyValue getKeyAtDirection(KeyboardData.Key k, int direction)
{
int i = DIRECTION_TO_INDEX[direction];
if (k.keys[i] == null)
return null;
return k.keys[i].kv;
}
/* /*
* Get the KeyValue at the given direction. In case of swipe (!= 0), get the * Get the KeyValue at the given direction. In case of swipe (direction !=
* nearest KeyValue that is not key0. * null), get the nearest KeyValue that is not key0.
* Take care of applying [_handler.onPointerSwipe] to the selected key, this * 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. * 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 * Return [null] if no key could be found in the given direction or if the
* selected key didn't change. * selected key didn't change.
*/ */
private KeyValue getKeyAtDirection(Pointer ptr, int direction) private KeyValue getNearestKeyAtDirection(Pointer ptr, Integer direction)
{ {
if (direction == 0) if (direction == null)
return handleKV(ptr.key.key0, ptr.modifiers); return handleKV(ptr.key.keys[0], ptr.modifiers);
KeyValue k; KeyValue k;
for (int i = 0; i > -3; i = (~i>>31) - i) // [i] is [0, -1, 1, -2, 2, ...]
for (int i = 0; i > -4; i = (~i>>31) - i)
{ {
int d = (direction + i + 8 - 1) % 8 + 1; int d = (direction + i + 16 - 1) % 16;
// Don't make the difference between a key that doesn't exist and a key // Don't make the difference between a key that doesn't exist and a key
// that is removed by [_handler]. Triggers side effects. // that is removed by [_handler]. Triggers side effects.
k = _handler.modifyKey(ptr.key.getAtDirection(d), ptr.modifiers); k = _handler.modifyKey(getKeyAtDirection(ptr.key, d), ptr.modifiers);
if (k != null) if (k != null)
return k; return k;
} }
@ -225,45 +242,32 @@ public final class Pointers implements Handler.Callback
} }
float dist = Math.abs(dx) + Math.abs(dy); float dist = Math.abs(dx) + Math.abs(dy);
int direction; Integer direction;
if (dist < _config.swipe_dist_px) if (dist < _config.swipe_dist_px)
{ {
direction = 0; direction = null;
} }
else else
{ {
// One of the 8 directions: // See [getKeyAtDirection()] for the meaning. The starting point on the
// |\2|3/| // circle is the top direction.
// |1\|/4| double a = Math.atan2(dy, dx); // between -pi and +pi, 0 is to the right
// |-----| direction = ((int)(a * 16 / (Math.PI * 2)) + 21) % 16;
// |8/|\5|
// |/7|6\|
direction = 1;
if (dx > 0) direction += 2;
if (dx > Math.abs(dy) || (dx < 0 && dx > -Math.abs(dy))) direction += 1;
if (dy > 0) direction = 9 - direction;
} }
if (direction != ptr.selected_direction) if (direction != ptr.selected_direction)
{ {
ptr.selected_direction = direction; ptr.selected_direction = direction;
KeyValue newValue = getKeyAtDirection(ptr, direction); KeyValue newValue = getNearestKeyAtDirection(ptr, direction);
if (newValue != null && (ptr.value == null || !newValue.equals(ptr.value))) if (newValue != null && !newValue.equals(ptr.value))
{ {
ptr.value = newValue; ptr.value = newValue;
ptr.flags = newValue.getFlags(); ptr.flags = newValue.getFlags();
// Sliding mode is entered when key2 or key3 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 &&
(ptr.key.hasValue(newValue, 5) || ptr.key.hasValue(newValue, 6)))
{ {
switch (direction)
{
case 1:
case 8:
case 4:
case 5:
startSliding(ptr, dy); startSliding(ptr, dy);
break;
}
} }
_handler.onPointerDown(true); _handler.onPointerDown(true);
} }
@ -419,9 +423,8 @@ public final class Pointers implements Handler.Callback
int count = (int)(dx / _config.slide_step_px); int count = (int)(dx / _config.slide_step_px);
if (count == ptr.sliding_count) if (count == ptr.sliding_count)
return; return;
KeyValue newValue = handleKV( int key_index = (count < ptr.sliding_count) ? 5 : 6;
(count < ptr.sliding_count) ? ptr.key.key3 : ptr.key.key2, KeyValue newValue = handleKV(ptr.key.keys[key_index], ptr.modifiers);
ptr.modifiers);
ptr.sliding_count = count; ptr.sliding_count = count;
ptr.value = newValue; ptr.value = newValue;
if (newValue != null) if (newValue != null)
@ -434,8 +437,8 @@ public final class Pointers implements Handler.Callback
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. */ /** Current direction. [null] means not swiping. */
public int selected_direction; public Integer selected_direction;
/** Selected value with [modifiers] applied. */ /** Selected value with [modifiers] applied. */
public KeyValue value; public KeyValue value;
public float downX; public float downX;
@ -455,7 +458,7 @@ public final class Pointers implements Handler.Callback
{ {
pointerId = p; pointerId = p;
key = k; key = k;
selected_direction = 0; selected_direction = null;
value = v; value = v;
downX = x; downX = x;
downY = y; downY = y;