From 68be82a4f92f47300b9960cf9cf65040c96f17ed Mon Sep 17 00:00:00 2001 From: Jules Aguillon Date: Sun, 23 Feb 2025 12:12:29 +0100 Subject: Macro keys (#878) Add "macro" keys that behave as if a sequence of keys is typed. Macro can be added to custom layouts or through the "Add keys to the keyboard option". The syntax is: symbol:key1,key2,.. The symbol cannot contain a : character. 'key1', 'key2', etc.. are: - 'String with \' escaping' The key will generate the specified string. - keyevent:123 The key will send a keyevent. - The name of any special key --- srcs/juloo.keyboard2/Autocapitalisation.java | 18 ++ srcs/juloo.keyboard2/KeyEventHandler.java | 34 ++- srcs/juloo.keyboard2/KeyValue.java | 64 ++++- srcs/juloo.keyboard2/KeyValueParser.java | 299 +++++++++++++++------ .../prefs/CustomExtraKeysPreference.java | 2 +- 5 files changed, 313 insertions(+), 104 deletions(-) (limited to 'srcs/juloo.keyboard2') diff --git a/srcs/juloo.keyboard2/Autocapitalisation.java b/srcs/juloo.keyboard2/Autocapitalisation.java index bf28e9d..a77d2e5 100644 --- a/srcs/juloo.keyboard2/Autocapitalisation.java +++ b/srcs/juloo.keyboard2/Autocapitalisation.java @@ -88,6 +88,24 @@ public final class Autocapitalisation callback_now(true); } + /** Pause auto capitalisation until [unpause()] is called. */ + public boolean pause() + { + boolean was_enabled = _enabled; + stop(); + _enabled = false; + return was_enabled; + } + + /** Continue auto capitalisation after [pause()] was called. Argument is the + output of [pause()]. */ + public void unpause(boolean was_enabled) + { + _enabled = was_enabled; + _should_update_caps_mode = true; + callback_now(true); + } + public static interface Callback { public void update_shift_state(boolean should_enable, boolean should_disable); diff --git a/srcs/juloo.keyboard2/KeyEventHandler.java b/srcs/juloo.keyboard2/KeyEventHandler.java index 6809d88..d2546ab 100644 --- a/srcs/juloo.keyboard2/KeyEventHandler.java +++ b/srcs/juloo.keyboard2/KeyEventHandler.java @@ -97,11 +97,10 @@ public final class KeyEventHandler case Keyevent: send_key_down_up(key.getKeyevent()); break; case Modifier: break; case Editing: handle_editing_key(key.getEditing()); break; - case Compose_pending: - _recv.set_compose_pending(true); - break; + case Compose_pending: _recv.set_compose_pending(true); break; case Slider: handle_slider(key.getSlider(), key.getSliderRepeat()); break; case StringWithSymbol: send_text(key.getStringWithSymbol()); break; + case Macro: evaluate_macro(key.getMacro()); break; } update_meta_state(old_mods); } @@ -319,6 +318,35 @@ public final class KeyEventHandler send_key_down_up_repeat(KeyEvent.KEYCODE_DPAD_DOWN, d); } + void evaluate_macro(KeyValue[] keys) + { + final Pointers.Modifiers empty = Pointers.Modifiers.EMPTY; + // Ignore modifiers that are activated at the time the macro is evaluated + mods_changed(empty); + Pointers.Modifiers mods = empty; + final boolean autocap_paused = _autocap.pause(); + for (KeyValue kv : keys) + { + kv = KeyModifier.modify(kv, mods); + if (kv == null) + continue; + if (kv.hasFlagsAny(KeyValue.FLAG_LATCH)) + { + // Non-special latchable keys clear latched modifiers + if (!kv.hasFlagsAny(KeyValue.FLAG_SPECIAL)) + mods = empty; + mods = mods.with_extra_mod(kv); + } + else + { + key_down(kv, false); + key_up(kv, mods); + mods = empty; + } + } + _autocap.unpause(autocap_paused); + } + /** Repeat calls to [send_key_down_up]. */ void send_key_down_up_repeat(int event_code, int repeat) { diff --git a/srcs/juloo.keyboard2/KeyValue.java b/srcs/juloo.keyboard2/KeyValue.java index 60d49e4..b2ef5db 100644 --- a/srcs/juloo.keyboard2/KeyValue.java +++ b/srcs/juloo.keyboard2/KeyValue.java @@ -96,6 +96,7 @@ public final class KeyValue implements Comparable String, // [_payload] is also the string to output, value is unused. Slider, // [_payload] is a [KeyValue.Slider], value is slider repeatition. StringWithSymbol, // [_payload] is a [KeyValue.StringWithSymbol], value is unused. + Macro, // [_payload] is a [KeyValue.Macro], value is unused. } private static final int FLAGS_OFFSET = 20; @@ -105,7 +106,8 @@ public final class KeyValue implements Comparable public static final int FLAG_LATCH = (1 << FLAGS_OFFSET << 0); // Key can be locked by typing twice when enabled in settings public static final int FLAG_DOUBLE_TAP_LOCK = (1 << FLAGS_OFFSET << 1); - // Special keys are not repeated and don't clear latched modifiers. + // Special keys are not repeated. + // Special latchable keys don't clear latched modifiers. public static final int FLAG_SPECIAL = (1 << FLAGS_OFFSET << 2); // Whether the symbol should be greyed out. For example, keys that are not // part of the pending compose sequence. @@ -229,6 +231,12 @@ public final class KeyValue implements Comparable return ((StringWithSymbol)_payload).str; } + /** Defined only when [getKind() == Kind.Macro]. */ + public KeyValue[] getMacro() + { + return ((Macro)_payload).keys; + } + /* Update the char and the symbol. */ public KeyValue withChar(char c) { @@ -460,31 +468,35 @@ public final class KeyValue implements Comparable Kind.StringWithSymbol, 0, flags); } + public static KeyValue makeMacro(String symbol, KeyValue[] keys, int flags) + { + return new KeyValue(new Macro(keys, symbol), Kind.Macro, 0, flags); + } + /** Make a modifier key for passing to [KeyModifier]. */ public static KeyValue makeInternalModifier(Modifier mod) { return new KeyValue("", Kind.Modifier, mod.ordinal(), 0); } - public static KeyValue parseKeyDefinition(String str) + /** Return a key by its name. If the given name doesn't correspond to any + special key, it is parsed with [KeyValueParser]. */ + public static KeyValue getKeyByName(String name) { - if (str.length() < 2 || str.charAt(0) != ':') - return makeStringKey(str); + KeyValue k = getSpecialKeyByName(name); + if (k != null) + return k; try { - return KeyValueParser.parse(str); + return KeyValueParser.parse(name); } catch (KeyValueParser.ParseError _e) { - return makeStringKey(str); + return makeStringKey(name); } } - /** - * Return a key by its name. If the given name doesn't correspond to a key - * defined in this function, it is passed to [parseStringKey] as a fallback. - */ - public static KeyValue getKeyByName(String name) + public static KeyValue getSpecialKeyByName(String name) { switch (name) { @@ -735,8 +747,7 @@ public final class KeyValue implements Comparable case "௲": case "௳": return makeStringKey(name, FLAG_SMALLER_FONT); - /* The key is not one of the special ones. */ - default: return parseKeyDefinition(name); + default: return null; } } @@ -787,4 +798,31 @@ public final class KeyValue implements Comparable @Override public String toString() { return symbol; } }; + + public static final class Macro implements Comparable + { + public final KeyValue[] keys; + private final String _symbol; + + public Macro(KeyValue[] keys_, String sym_) + { + keys = keys_; + _symbol = sym_; + } + + public String toString() { return _symbol; } + + @Override + public int compareTo(Macro snd) + { + int d = keys.length - snd.keys.length; + if (d != 0) return d; + for (int i = 0; i < keys.length; i++) + { + d = keys[i].compareTo(snd.keys[i]); + if (d != 0) return d; + } + return _symbol.compareTo(snd._symbol); + } + }; } diff --git a/srcs/juloo.keyboard2/KeyValueParser.java b/srcs/juloo.keyboard2/KeyValueParser.java index 0a5ce17..488f5d3 100644 --- a/srcs/juloo.keyboard2/KeyValueParser.java +++ b/srcs/juloo.keyboard2/KeyValueParser.java @@ -1,14 +1,22 @@ package juloo.keyboard2; +import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Parse a key definition. The syntax for a key definition is: +- [(symbol):(key_action)] - [:(kind) (attributes):(payload)]. - If [str] doesn't start with a [:] character, it is interpreted as an arbitrary string key. +[key_action] is: +- ['Arbitrary string'] +- [(key_action),(key_action),...] +- [keyevent:(code)] +- [(key_name)] + For the different kinds and attributes, see doc/Possible-key-values.md. Examples: @@ -18,103 +26,232 @@ Examples: */ public final class KeyValueParser { - static Pattern START_PAT; - static Pattern ATTR_PAT; + static Pattern KEYDEF_TOKEN; static Pattern QUOTED_PAT; - static Pattern PAYLOAD_START_PAT; static Pattern WORD_PAT; - static public KeyValue parse(String str) throws ParseError + static public KeyValue parse(String input) throws ParseError { - String symbol = null; - int flags = 0; + int symbol_ends = 0; + final int input_len = input.length(); + while (symbol_ends < input_len && input.charAt(symbol_ends) != ':') + symbol_ends++; + if (symbol_ends == 0) // Old syntax + return Starting_with_colon.parse(input); + if (symbol_ends == input_len) // String key + return KeyValue.makeStringKey(input); + String symbol = input.substring(0, symbol_ends); + ArrayList keydefs = new ArrayList(); init(); - // Kind - Matcher m = START_PAT.matcher(str); - if (!m.lookingAt()) - parseError("Expected kind, for example \":str ...\".", m); - String kind = m.group(1); - // Attributes - while (true) - { - if (!match(m, ATTR_PAT)) - break; - String attr_name = m.group(1); - String attr_value = parseSingleQuotedString(m); - switch (attr_name) - { - case "flags": - flags = parseFlags(attr_value, m); - break; - case "symbol": - symbol = attr_value; - break; + Matcher m = KEYDEF_TOKEN.matcher(input); + m.region(symbol_ends + 1, input_len); + do { keydefs.add(parse_key_def(m)); } + while (parse_comma(m)); + for (KeyValue k : keydefs) + if (k == null) + parseError("Contains null key", m); + return KeyValue.makeMacro(symbol, keydefs.toArray(new KeyValue[]{}), 0); + } - default: - parseError("Unknown attribute "+attr_name, m); - } - } - // Payload - if (!match(m, PAYLOAD_START_PAT)) - parseError("Unexpected character", m); - String payload; - switch (kind) + static void init() + { + if (KEYDEF_TOKEN != null) + return; + KEYDEF_TOKEN = Pattern.compile("'|,|keyevent:|(?:[^\\\\',]+|\\\\.)+"); + QUOTED_PAT = Pattern.compile("((?:[^'\\\\]+|\\\\')*)'"); + WORD_PAT = Pattern.compile("[a-zA-Z0-9_]+|."); + } + + static KeyValue key_by_name_or_str(String str) + { + KeyValue k = KeyValue.getSpecialKeyByName(str); + if (k != null) + return k; + return KeyValue.makeStringKey(str); + } + + static KeyValue parse_key_def(Matcher m) throws ParseError + { + if (!match(m, KEYDEF_TOKEN)) + parseError("Expected key definition", m); + String token = m.group(0); + switch (token) { - case "str": - payload = parseSingleQuotedString(m); - if (symbol == null) - return KeyValue.makeStringKey(payload, flags); - return KeyValue.makeStringKeyWithSymbol(payload, symbol, flags); - - case "char": - payload = parsePayloadWord(m); - if (payload.length() != 1) - parseError("Expected a single character payload", m); - return KeyValue.makeCharKey(payload.charAt(0), symbol, flags); - - case "keyevent": - payload = parsePayloadWord(m); - int eventcode = 0; - try { eventcode = Integer.parseInt(payload); } - catch (Exception _e) - { parseError("Expected an integer payload", m); } - if (symbol == null) - symbol = String.valueOf(eventcode); - return KeyValue.keyeventKey(symbol, eventcode, flags); - - default: break; + case "'": return parse_string_keydef(m); + case ",": parseError("Unexpected comma", m); return null; + case "keyevent:": return parse_keyevent_keydef(m); + default: return key_by_name_or_str(remove_escaping(token)); } - parseError("Unknown kind '"+kind+"'", m, 1); - return null; // Unreachable } - static String parseSingleQuotedString(Matcher m) throws ParseError + static KeyValue parse_string_keydef(Matcher m) throws ParseError { if (!match(m, QUOTED_PAT)) - parseError("Expected quoted string", m); - return m.group(1).replace("\\'", "'"); + parseError("Unterminated quoted string", m); + return KeyValue.makeStringKey(remove_escaping(m.group(1))); } - static String parsePayloadWord(Matcher m) throws ParseError + static KeyValue parse_keyevent_keydef(Matcher m) throws ParseError { if (!match(m, WORD_PAT)) - parseError("Expected a word after ':' made of [a-zA-Z0-9_]", m); - return m.group(0); + parseError("Expected keyevent code", m); + int eventcode = 0; + try { eventcode = Integer.parseInt(m.group(0)); } + catch (Exception _e) + { parseError("Expected an integer payload", m); } + return KeyValue.keyeventKey("", eventcode, 0); + } + + /** Returns [true] if the next token is a comma, [false] if it is the end of the input. Throws an error otherwise. */ + static boolean parse_comma(Matcher m) throws ParseError + { + if (!match(m, KEYDEF_TOKEN)) + return false; + String token = m.group(0); + if (!token.equals(",")) + parseError("Expected comma instead of '"+ token + "'", m); + return true; + } + + static String remove_escaping(String s) + { + if (!s.contains("\\")) + return s; + StringBuilder out = new StringBuilder(s.length()); + final int len = s.length(); + int prev = 0, i = 0; + for (; i < len; i++) + if (s.charAt(i) == '\\') + { + out.append(s, prev, i); + prev = i + 1; + } + out.append(s, prev, i); + return out.toString(); } - static int parseFlags(String s, Matcher m) throws ParseError + /** + Parse a key definition starting with a [:]. This is the old syntax and is + kept for compatibility. + */ + final static class Starting_with_colon { - int flags = 0; - for (String f : s.split(",")) + static Pattern START_PAT; + static Pattern ATTR_PAT; + static Pattern QUOTED_PAT; + static Pattern PAYLOAD_START_PAT; + static Pattern WORD_PAT; + + static public KeyValue parse(String str) throws ParseError { - switch (f) + String symbol = null; + int flags = 0; + init(); + // Kind + Matcher m = START_PAT.matcher(str); + if (!m.lookingAt()) + parseError("Expected kind, for example \":str ...\".", m); + String kind = m.group(1); + // Attributes + while (true) { - case "dim": flags |= KeyValue.FLAG_SECONDARY; break; - case "small": flags |= KeyValue.FLAG_SMALLER_FONT; break; - default: parseError("Unknown flag "+f, m); + if (!match(m, ATTR_PAT)) + break; + String attr_name = m.group(1); + String attr_value = parseSingleQuotedString(m); + switch (attr_name) + { + case "flags": + flags = parseFlags(attr_value, m); + break; + case "symbol": + symbol = attr_value; + break; + + default: + parseError("Unknown attribute "+attr_name, m); + } } + // Payload + if (!match(m, PAYLOAD_START_PAT)) + parseError("Unexpected character", m); + String payload; + switch (kind) + { + case "str": + payload = parseSingleQuotedString(m); + if (symbol == null) + return KeyValue.makeStringKey(payload, flags); + return KeyValue.makeStringKeyWithSymbol(payload, symbol, flags); + + case "char": + payload = parsePayloadWord(m); + if (payload.length() != 1) + parseError("Expected a single character payload", m); + return KeyValue.makeCharKey(payload.charAt(0), symbol, flags); + + case "keyevent": + payload = parsePayloadWord(m); + int eventcode = 0; + try { eventcode = Integer.parseInt(payload); } + catch (Exception _e) + { parseError("Expected an integer payload", m); } + if (symbol == null) + symbol = String.valueOf(eventcode); + return KeyValue.keyeventKey(symbol, eventcode, flags); + + default: break; + } + parseError("Unknown kind '"+kind+"'", m, 1); + return null; // Unreachable + } + + static String parseSingleQuotedString(Matcher m) throws ParseError + { + if (!match(m, QUOTED_PAT)) + parseError("Expected quoted string", m); + return m.group(1).replace("\\'", "'"); + } + + static String parsePayloadWord(Matcher m) throws ParseError + { + if (!match(m, WORD_PAT)) + parseError("Expected a word after ':' made of [a-zA-Z0-9_]", m); + return m.group(0); + } + + static int parseFlags(String s, Matcher m) throws ParseError + { + int flags = 0; + for (String f : s.split(",")) + { + switch (f) + { + case "dim": flags |= KeyValue.FLAG_SECONDARY; break; + case "small": flags |= KeyValue.FLAG_SMALLER_FONT; break; + default: parseError("Unknown flag "+f, m); + } + } + return flags; + } + + static boolean match(Matcher m, Pattern pat) + { + try { m.region(m.end(), m.regionEnd()); } catch (Exception _e) {} + m.usePattern(pat); + return m.lookingAt(); + } + + static void init() + { + if (START_PAT != null) + return; + START_PAT = Pattern.compile(":(\\w+)"); + ATTR_PAT = Pattern.compile("\\s*(\\w+)\\s*="); + QUOTED_PAT = Pattern.compile("'(([^'\\\\]+|\\\\')*)'"); + PAYLOAD_START_PAT = Pattern.compile("\\s*:"); + WORD_PAT = Pattern.compile("[a-zA-Z0-9_]*"); } - return flags; } static boolean match(Matcher m, Pattern pat) @@ -124,17 +261,6 @@ public final class KeyValueParser return m.lookingAt(); } - static void init() - { - if (START_PAT != null) - return; - START_PAT = Pattern.compile(":(\\w+)"); - ATTR_PAT = Pattern.compile("\\s*(\\w+)\\s*="); - QUOTED_PAT = Pattern.compile("'(([^'\\\\]+|\\\\')*)'"); - PAYLOAD_START_PAT = Pattern.compile("\\s*:"); - WORD_PAT = Pattern.compile("[a-zA-Z0-9_]*"); - } - static void parseError(String msg, Matcher m) throws ParseError { parseError(msg, m, m.regionStart()); @@ -145,8 +271,7 @@ public final class KeyValueParser StringBuilder msg_ = new StringBuilder("Syntax error"); try { - char c = m.group(0).charAt(0); - msg_.append(" at character '").append(c).append("'"); + msg_.append(" at token '").append(m.group(0)).append("'"); } catch (IllegalStateException _e) {} msg_.append(" at position "); msg_.append(i); diff --git a/srcs/juloo.keyboard2/prefs/CustomExtraKeysPreference.java b/srcs/juloo.keyboard2/prefs/CustomExtraKeysPreference.java index 6f4cf41..253e074 100644 --- a/srcs/juloo.keyboard2/prefs/CustomExtraKeysPreference.java +++ b/srcs/juloo.keyboard2/prefs/CustomExtraKeysPreference.java @@ -41,7 +41,7 @@ public class CustomExtraKeysPreference extends ListGroupPreference if (key_names != null) { for (String key_name : key_names) - kvs.put(KeyValue.parseKeyDefinition(key_name), KeyboardData.PreferredPos.DEFAULT); + kvs.put(KeyValue.getKeyByName(key_name), KeyboardData.PreferredPos.DEFAULT); } return kvs; } -- cgit v1.2.3