Compose key

The COMPOSE_PENDING modifier indicate whether a compose sequence is in
progress. The new key of kind Compose_pending sets the current state of
the sequence.

The compose sequences are compiled into a state machine by a python
script into a compact encoding.

The state of the pending compose is determined by the index of a state.
This commit is contained in:
Jules Aguillon 2024-02-11 20:46:36 +01:00
parent 38deb810f9
commit 8c29073260
11 changed files with 230 additions and 11 deletions

View File

@ -85,7 +85,6 @@ android {
}
dependencies {
}
tasks.register('buildKeyboardFont') {
@ -108,6 +107,7 @@ tasks.withType(Test).configureEach {
dependsOn 'genLayoutsList'
dependsOn 'checkKeyboardLayouts'
dependsOn 'syncTranslations'
dependsOn 'compileComposeSequences'
}
tasks.register('genLayoutsList') {
@ -138,6 +138,19 @@ tasks.register('syncTranslations') {
}
}
tasks.register('compileComposeSequences') {
def out = "srcs/juloo.keyboard2/ComposeKeyData.java"
println "\nGenerating ${out}"
exec {
def sequences = new File(projectDir, "srcs/compose").listFiles().findAll {
it.name.endsWith(".txt")
}
workingDir = projectDir
commandLine("python", "srcs/compose/compile.py", *sequences)
standardOutput = new FileOutputStream("${projectDir}/${out}")
}
}
tasks.named("preBuild") {
dependsOn += "initDebugKeystore"
dependsOn += "copyRawQwertyUS"

View File

@ -3,6 +3,6 @@
<key width="1.7" key0="ctrl" key1="loc switch_greekmath" key2="loc meta" key4="switch_numeric"/>
<key width="1.1" key0="fn" key1="loc alt" key2="loc change_method" key3="switch_emoji" key4="config"/>
<key width="4.4" key0="space" key7="switch_forward" key8="switch_backward" key5="cursor_left" key6="cursor_right" slider="true"/>
<key width="1.1" key7="up" key6="right" key5="left" key8="down" key1="loc home" key2="loc page_up" key3="loc end" key4="loc page_down"/>
<key width="1.1" key0="compose" key7="up" key6="right" key5="left" key8="down" key1="loc home" key2="loc page_up" key3="loc end" key4="loc page_down"/>
<key width="1.7" key0="enter" key1="loc voice_typing" key2="action"/>
</row>

80
srcs/compose/compile.py Normal file
View File

@ -0,0 +1,80 @@
import textwrap, sys
def parse_sequences_file(fname):
with open(fname, "r") as inp:
return [ (s[:-2], s[-2]) for s in inp if len(s) > 1 ]
# Turn a list of sequences into a trie.
def add_sequences_to_trie(seqs, trie):
for seq, result in seqs:
t_ = trie
i = 0
while i < len(seq) - 1:
c = seq[i]
if c not in t_:
t_[c] = {}
t_ = t_[c]
i += 1
c = seq[i]
t_[c] = result
# Compile the trie into a state machine.
def make_automata(tree_root):
states = []
def add_tree(t):
# Index and size of the new node
i = len(states)
s = len(t.keys())
# Add node header
states.append((0, s + 1))
i += 1
# Reserve space for the current node in both arrays
for c in range(s):
states.append((None, None))
# Add nested nodes and fill the current node
for c in sorted(t.keys()):
node_i = len(states)
add_node(t[c])
states[i] = (c, node_i)
i += 1
def add_leaf(c):
states.append((c, 1))
def add_node(n):
if type(n) == str:
add_leaf(n)
else:
add_tree(n)
add_tree(tree_root)
return states
# Print the state machine compiled by make_automata into java code that can be
# used by [ComposeKeyData.java].
def gen_java(machine):
def gen_array(array, indent):
return textwrap.fill(", ".join(map(str, array)), subsequent_indent=indent)
print("""package juloo.keyboard2;
/** This file is generated, see [srcs/compose/compile.py]. */
public final class ComposeKeyData
{
public static final char[] states = {
%s
};
public static final short[] edges = {
%s
};
}""" % (
gen_array(map(lambda s: repr(s[0]), machine), ' '),
gen_array(map(lambda s: s[1], machine), ' '),
))
total_sequences = 0
trie = {}
for fname in sys.argv[1:]:
sequences = parse_sequences_file(fname)
add_sequences_to_trie(sequences, trie)
total_sequences += len(sequences)
gen_java(make_automata(trie))
print("Compiled %d sequences" % total_sequences, file=sys.stderr)

View File

@ -0,0 +1,4 @@
=e€
`eè
`aà
`uù

View File

@ -0,0 +1,55 @@
package juloo.keyboard2;
import java.util.Arrays;
public final class ComposeKey
{
/** Apply the pending compose sequence to [kv]. Returns [null] if [kv] is not
part of the pending sequence. */
public static KeyValue apply(int state, KeyValue kv)
{
switch (kv.getKind())
{
case Char: return apply(state, kv.getChar());
/* These keys must not be removed. */
case Event: return kv;
case Modifier: return kv;
/* These keys cannot be part of sequences. */
case String: return null;
case Keyevent: return null;
case Editing: return null;
case Placeholder: return null;
case Compose_pending: return null;
}
return null;
}
/** Apply the pending compose sequence to char [c]. */
static KeyValue apply(int state, char c)
{
char[] states = ComposeKeyData.states;
short[] edges = ComposeKeyData.edges;
int length = edges[state];
int next = Arrays.binarySearch(states, state + 1, state + length, c);
if (next < 0)
return null;
next = edges[next];
// The next state is the end of a sequence, show the result.
if (edges[next] == 1)
return KeyValue.makeCharKey(states[next]);
return KeyValue.makeComposePending(String.valueOf(c), next, 0);
}
/** The [states] array represents the different states and their transition.
A state occupies one or several cells of the array:
- The first cell is the result of the conpose sequence if the state is of
size 1, [0] otherwise.
- The remaining cells are the transitions, sorted alphabetically.
The [edges] array represents the transition state corresponding to each
accepted inputs.
Id [states[i]] is the first cell of a state, [edges[i]] is the number of
cells occupied by the state [i].
If [states[i]] is a transition, [edges[i]] is the index of the state to
jump into. */
}

Binary file not shown.

View File

@ -69,6 +69,9 @@ public final class KeyEventHandler implements Config.IKeyEventHandler
case META:
_autocap.stop();
break;
case COMPOSE_PENDING:
KeyModifier.set_compose_pending(0);
break;
}
break;
default: break;
@ -91,6 +94,10 @@ public final class KeyEventHandler implements Config.IKeyEventHandler
case Keyevent: send_key_down_up(key.getKeyevent()); break;
case Modifier: break;
case Editing: handle_editing_key(key.getEditing()); break;
case Compose_pending:
KeyModifier.set_compose_pending(key.getPendingCompose());
_recv.set_compose_pending(true);
break;
}
update_meta_state(old_mods);
}
@ -293,6 +300,7 @@ public final class KeyEventHandler implements Config.IKeyEventHandler
{
public void handle_event_key(KeyValue.Event ev);
public void set_shift_state(boolean state, boolean lock);
public void set_compose_pending(boolean pending);
public InputConnection getCurrentInputConnection();
}

View File

@ -10,6 +10,10 @@ public final class KeyModifier
private static HashMap<KeyValue, HashMap<Pointers.Modifiers, KeyValue>> _cache =
new HashMap<KeyValue, HashMap<Pointers.Modifiers, KeyValue>>();
/** The current compose state. Whether a compose is pending is signaled by
the [COMPOSE_PENDING] modifier. */
static int _compose_pending = -1;
/** Modify a key according to modifiers. */
public static KeyValue modify(KeyValue k, Pointers.Modifiers mods)
{
@ -27,7 +31,11 @@ public final class KeyModifier
ks.put(mods, r);
}
/* Keys with an empty string are placeholder keys. */
return (r.getString().length() == 0) ? null : r;
if (r.getString().length() == 0)
return null;
if (mods.has(KeyValue.Modifier.COMPOSE_PENDING))
r = ComposeKey.apply(_compose_pending, r);
return r;
}
public static KeyValue modify(KeyValue k, KeyValue.Modifier mod)
@ -99,6 +107,11 @@ public final class KeyModifier
}
}
public static void set_compose_pending(int state)
{
_compose_pending = state;
}
private static KeyValue apply_map_char(KeyValue k, Map_char map)
{
switch (k.getKind())

View File

@ -26,6 +26,7 @@ public final class KeyValue
// Must be evaluated in the reverse order of their values.
public static enum Modifier
{
COMPOSE_PENDING,
SHIFT,
CTRL,
ALT,
@ -88,7 +89,8 @@ public final class KeyValue
public static enum Kind
{
Char, String, Keyevent, Event, Modifier, Editing, Placeholder
Char, String, Keyevent, Event, Modifier, Editing, Placeholder,
Compose_pending
}
// Behavior flags.
@ -172,11 +174,18 @@ public final class KeyValue
return Editing.values()[(_code & VALUE_BITS)];
}
/** Defined only when [getKind() == Kind.Placeholder]. */
public Placeholder getPlaceholder()
{
return Placeholder.values()[(_code & VALUE_BITS)];
}
/** Defined only when [getKind() == Kind.Compose_pending]. */
public int getPendingCompose()
{
return (_code & VALUE_BITS);
}
/* Update the char and the symbol. */
public KeyValue withChar(char c)
{
@ -303,6 +312,17 @@ public final class KeyValue
return makeStringKey(str, 0);
}
public static KeyValue makeCharKey(char c)
{
return new KeyValue(String.valueOf(c), Kind.Char, c, 0);
}
public static KeyValue makeComposePending(String symbol, int state, int flags)
{
return new KeyValue(symbol, Kind.Compose_pending, state,
flags | FLAG_SPECIAL);
}
/** Make a key that types a string. A char key is returned for a string of
length 1. */
public static KeyValue makeStringKey(String str, int flags)
@ -464,6 +484,9 @@ public final class KeyValue
case "textAssist": return editingKey(0xE038, Editing.ASSIST);
case "autofill": return editingKey("auto", Editing.AUTOFILL);
/* The compose key */
case "compose": return modifierKey("comp", Modifier.COMPOSE_PENDING, FLAG_SECONDARY);
/* Placeholder keys */
case "removed": return placeholderKey(Placeholder.REMOVED);
case "f11_placeholder": return placeholderKey(Placeholder.F11);

View File

@ -440,6 +440,11 @@ public class Keyboard2 extends InputMethodService
_keyboardView.set_shift_state(state, lock);
}
public void set_compose_pending(boolean pending)
{
_keyboardView.set_compose_pending(pending);
}
public InputConnection getCurrentInputConnection()
{
return Keyboard2.this.getCurrentInputConnection();

View File

@ -25,6 +25,10 @@ public class Keyboard2View extends View
private KeyValue _shift_kv;
private KeyboardData.Key _shift_key;
/** Used to add fake pointers. */
private KeyValue _compose_kv;
private KeyboardData.Key _compose_key;
private Pointers _pointers;
private Pointers.Modifiers _mods;
@ -98,6 +102,8 @@ public class Keyboard2View extends View
_shift_kv = _shift_kv.withFlags(_shift_kv.getFlags() | KeyValue.FLAG_LOCK);
_shift_key = _keyboard.findKeyWithValue(_shift_kv);
}
_compose_kv = KeyValue.getKeyByName("compose");
_compose_key = _keyboard.findKeyWithValue(_compose_kv);
reset();
}
@ -109,26 +115,38 @@ public class Keyboard2View extends View
invalidate();
}
/** Called by auto-capitalisation. */
public void set_shift_state(boolean state, boolean lock)
void set_fake_ptr_latched(KeyboardData.Key key, KeyValue kv, boolean latched,
boolean lock)
{
if (_keyboard == null || _shift_key == null)
if (_keyboard == null || key == null)
return;
int flags = _pointers.getKeyFlags(_shift_key, _shift_kv);
if (state)
int flags = _pointers.getKeyFlags(key, kv);
if (latched)
{
if (flags != -1 && !lock)
return; // Don't replace an existing pointer
_pointers.add_fake_pointer(_shift_kv, _shift_key, lock);
_pointers.add_fake_pointer(kv, key, lock);
}
else
{
if ((flags & KeyValue.FLAG_FAKE_PTR) == 0)
return; // Don't remove locked pointers
_pointers.remove_fake_pointer(_shift_kv, _shift_key);
_pointers.remove_fake_pointer(kv, key);
}
}
/** Called by auto-capitalisation. */
public void set_shift_state(boolean latched, boolean lock)
{
set_fake_ptr_latched(_shift_key, _shift_kv, latched, lock);
}
/** Called from [KeyEventHandler]. */
public void set_compose_pending(boolean pending)
{
set_fake_ptr_latched(_compose_key, _compose_kv, pending, false);
}
public KeyValue modifyKey(KeyValue k, Pointers.Modifiers mods)
{
if (_keyboard.modmap != null)