From 2b9d01a406c6d078c541b649c3218fba95db6dd6 Mon Sep 17 00:00:00 2001 From: Jules Aguillon Date: Sun, 11 Feb 2024 20:46:36 +0100 Subject: [PATCH] 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. --- build.gradle | 15 ++++- res/xml/bottom_row.xml | 2 +- srcs/compose/compile.py | 80 +++++++++++++++++++++++ srcs/compose/sequences.txt | 4 ++ srcs/juloo.keyboard2/ComposeKey.java | 55 ++++++++++++++++ srcs/juloo.keyboard2/ComposeKeyData.java | 14 ++++ srcs/juloo.keyboard2/KeyEventHandler.java | 8 +++ srcs/juloo.keyboard2/KeyModifier.java | 15 ++++- srcs/juloo.keyboard2/KeyValue.java | 25 ++++++- srcs/juloo.keyboard2/Keyboard2.java | 5 ++ srcs/juloo.keyboard2/Keyboard2View.java | 32 +++++++-- 11 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 srcs/compose/compile.py create mode 100644 srcs/compose/sequences.txt create mode 100644 srcs/juloo.keyboard2/ComposeKey.java create mode 100644 srcs/juloo.keyboard2/ComposeKeyData.java diff --git a/build.gradle b/build.gradle index 139b27bed..6544bb5d4 100644 --- a/build.gradle +++ b/build.gradle @@ -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" diff --git a/res/xml/bottom_row.xml b/res/xml/bottom_row.xml index 33e381d40..d0562d13d 100644 --- a/res/xml/bottom_row.xml +++ b/res/xml/bottom_row.xml @@ -3,6 +3,6 @@ - + diff --git a/srcs/compose/compile.py b/srcs/compose/compile.py new file mode 100644 index 000000000..214d4b8ba --- /dev/null +++ b/srcs/compose/compile.py @@ -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) diff --git a/srcs/compose/sequences.txt b/srcs/compose/sequences.txt new file mode 100644 index 000000000..31063b752 --- /dev/null +++ b/srcs/compose/sequences.txt @@ -0,0 +1,4 @@ +=e€ +`eè +`aà +`uù diff --git a/srcs/juloo.keyboard2/ComposeKey.java b/srcs/juloo.keyboard2/ComposeKey.java new file mode 100644 index 000000000..5b8956084 --- /dev/null +++ b/srcs/juloo.keyboard2/ComposeKey.java @@ -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. */ +} diff --git a/srcs/juloo.keyboard2/ComposeKeyData.java b/srcs/juloo.keyboard2/ComposeKeyData.java new file mode 100644 index 000000000..b73db603a --- /dev/null +++ b/srcs/juloo.keyboard2/ComposeKeyData.java @@ -0,0 +1,14 @@ +package juloo.keyboard2; + +/** This file is generated, see [srcs/compose/compile.py]. */ + +public final class ComposeKeyData +{ + public static final char[] states = { + 0, '=', '`', 0, 'e', '€', 0, 'a', 'e', 'u', 'à', 'è', 'ù' + }; + + public static final short[] edges = { + 3, 3, 6, 2, 5, 1, 4, 10, 11, 12, 1, 1, 1 + }; +} diff --git a/srcs/juloo.keyboard2/KeyEventHandler.java b/srcs/juloo.keyboard2/KeyEventHandler.java index 65de7796a..6cc7171e7 100644 --- a/srcs/juloo.keyboard2/KeyEventHandler.java +++ b/srcs/juloo.keyboard2/KeyEventHandler.java @@ -69,6 +69,9 @@ public void key_down(KeyValue key, boolean isSwipe) case META: _autocap.stop(); break; + case COMPOSE_PENDING: + KeyModifier.set_compose_pending(0); + break; } break; default: break; @@ -91,6 +94,10 @@ public void key_up(KeyValue key, Pointers.Modifiers mods) 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 static interface IReceiver { 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(); } diff --git a/srcs/juloo.keyboard2/KeyModifier.java b/srcs/juloo.keyboard2/KeyModifier.java index 51e70510d..94f096bcb 100644 --- a/srcs/juloo.keyboard2/KeyModifier.java +++ b/srcs/juloo.keyboard2/KeyModifier.java @@ -10,6 +10,10 @@ public final class KeyModifier private static HashMap> _cache = new HashMap>(); + /** 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 static KeyValue modify(KeyValue k, Pointers.Modifiers mods) 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 static Map_char modify_numpad_script(String numpad_script) } } + 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()) diff --git a/srcs/juloo.keyboard2/KeyValue.java b/srcs/juloo.keyboard2/KeyValue.java index dbd9e3ee4..e60e55454 100644 --- a/srcs/juloo.keyboard2/KeyValue.java +++ b/srcs/juloo.keyboard2/KeyValue.java @@ -26,6 +26,7 @@ public static enum Event // Must be evaluated in the reverse order of their values. public static enum Modifier { + COMPOSE_PENDING, SHIFT, CTRL, ALT, @@ -88,7 +89,8 @@ public static enum Placeholder 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 Editing getEditing() 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 static KeyValue makeStringKey(String str) 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 static KeyValue getKeyByName(String name) 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); diff --git a/srcs/juloo.keyboard2/Keyboard2.java b/srcs/juloo.keyboard2/Keyboard2.java index 6440c7de8..987684b87 100644 --- a/srcs/juloo.keyboard2/Keyboard2.java +++ b/srcs/juloo.keyboard2/Keyboard2.java @@ -434,6 +434,11 @@ public void set_shift_state(boolean state, boolean lock) _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(); diff --git a/srcs/juloo.keyboard2/Keyboard2View.java b/srcs/juloo.keyboard2/Keyboard2View.java index fb95412b4..88b718c8a 100644 --- a/srcs/juloo.keyboard2/Keyboard2View.java +++ b/srcs/juloo.keyboard2/Keyboard2View.java @@ -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 void setKeyboard(KeyboardData kw) _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 void reset() 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)