Skip to content

Commit

Permalink
WIP: Compose key
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Julow committed Feb 11, 2024
1 parent f4d88cc commit e6fbc72
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 4 deletions.
15 changes: 14 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ android {
}

dependencies {

}

tasks.register('buildKeyboardFont') {
Expand All @@ -108,6 +107,7 @@ tasks.withType(Test).configureEach {
dependsOn 'genLayoutsList'
dependsOn 'checkKeyboardLayouts'
dependsOn 'syncTranslations'
dependsOn 'compileComposeSequences'
}

tasks.register('genLayoutsList') {
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion res/xml/bottom_row.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 80 additions & 0 deletions srcs/compose/compile.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions srcs/compose/sequences.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
=e€
`eè
`aà
`uù
56 changes: 56 additions & 0 deletions srcs/juloo.keyboard2/ComposeKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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];
if (length == 1)
return KeyValue.makeCharKey(states[state]);
int next = Arrays.binarySearch(states, state + 1, length - 1, c);
if (next < 0)
return null;
// 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. */
}
14 changes: 14 additions & 0 deletions srcs/juloo.keyboard2/ComposeKeyData.java
Original file line number Diff line number Diff line change
@@ -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
};
}
6 changes: 6 additions & 0 deletions srcs/juloo.keyboard2/KeyEventHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ public void key_down(KeyValue key, boolean isSwipe)
case META:
_autocap.stop();
break;
case COMPOSE_PENDING:
KeyModifier.set_compose_pending(key.getPendingCompose());
break;
}
break;
default: break;
Expand All @@ -91,6 +94,9 @@ 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());
break;
}
update_meta_state(old_mods);
}
Expand Down
15 changes: 14 additions & 1 deletion srcs/juloo.keyboard2/KeyModifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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)
Expand Down Expand Up @@ -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())
Expand Down
25 changes: 24 additions & 1 deletion srcs/juloo.keyboard2/KeyValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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, 0);

/* Placeholder keys */
case "removed": return placeholderKey(Placeholder.REMOVED);
case "f11_placeholder": return placeholderKey(Placeholder.F11);
Expand Down

0 comments on commit e6fbc72

Please sign in to comment.