From ecd912eb214eca6234f5202ff6cee63a9596305f Mon Sep 17 00:00:00 2001 From: Jules Aguillon Date: Wed, 11 Dec 2024 13:52:54 +0100 Subject: [PATCH] WIP: Macro 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: :macro symbol='V':ctrl,v :macro symbol='CA':ctrl,a,ctrl,c :macro symbol='<<':ctrl,backspace The key syntax is changed slightly to make it delimited, creating more precise error messages. --- srcs/juloo.keyboard2/Autocapitalisation.java | 18 +++++ srcs/juloo.keyboard2/KeyEventHandler.java | 34 ++++++++- srcs/juloo.keyboard2/KeyValue.java | 52 +++++++++++++- srcs/juloo.keyboard2/KeyValueParser.java | 75 +++++++++++++++----- test/juloo.keyboard2/KeyValueParserTest.java | 26 +++++++ 5 files changed, 182 insertions(+), 23 deletions(-) diff --git a/srcs/juloo.keyboard2/Autocapitalisation.java b/srcs/juloo.keyboard2/Autocapitalisation.java index bf28e9d44..a77d2e51e 100644 --- a/srcs/juloo.keyboard2/Autocapitalisation.java +++ b/srcs/juloo.keyboard2/Autocapitalisation.java @@ -88,6 +88,24 @@ public void stop() 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 eba488a8e..8719fe040 100644 --- a/srcs/juloo.keyboard2/KeyEventHandler.java +++ b/srcs/juloo.keyboard2/KeyEventHandler.java @@ -96,11 +96,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: - _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); } @@ -316,6 +315,35 @@ void move_cursor_vertical(int d) 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 fea03faa3..acfdaf7e4 100644 --- a/srcs/juloo.keyboard2/KeyValue.java +++ b/srcs/juloo.keyboard2/KeyValue.java @@ -95,6 +95,7 @@ public static enum Kind 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 = 19; @@ -104,7 +105,8 @@ public static enum Kind 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 String getStringWithSymbol() 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) { @@ -454,6 +462,11 @@ public static KeyValue makeStringKeyWithSymbol(String str, String symbol, int fl 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) { @@ -479,6 +492,14 @@ public static KeyValue parseKeyDefinition(String str) * defined in this function, it is passed to [parseStringKey] as a fallback. */ public static KeyValue getKeyByName(String name) + { + KeyValue k = getSpecialKeyByName(name); + if (k == null) + return parseKeyDefinition(name); + return k; + } + + public static KeyValue getSpecialKeyByName(String name) { switch (name) { @@ -728,8 +749,7 @@ public static KeyValue getKeyByName(String name) case "௲": case "௳": return makeStringKey(name, FLAG_SMALLER_FONT); - /* The key is not one of the special ones. */ - default: return parseKeyDefinition(name); + default: return null; } } @@ -780,4 +800,30 @@ public static enum Slider @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; } + + 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 0a5ce17de..f46b1da6a 100644 --- a/srcs/juloo.keyboard2/KeyValueParser.java +++ b/srcs/juloo.keyboard2/KeyValueParser.java @@ -1,5 +1,6 @@ package juloo.keyboard2; +import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -23,18 +24,51 @@ public final class KeyValueParser static Pattern QUOTED_PAT; static Pattern PAYLOAD_START_PAT; static Pattern WORD_PAT; + static Pattern COMMA_PAT; + static Pattern END_OF_INPUT_PAT; static public KeyValue parse(String str) throws ParseError { - 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); + KeyValue k = parseKeyValue(m); + if (!match(m, END_OF_INPUT_PAT)) + parseError("Unexpected character", m); + return k; + } + + 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_]+|."); + COMMA_PAT = Pattern.compile(","); + END_OF_INPUT_PAT = Pattern.compile("$"); + } + + static KeyValue parseKeyValue(Matcher m) throws ParseError + { + if (match(m, START_PAT)) + return parseComplexKeyValue(m, m.group(1)); + // Key doesn't start with ':', accept either a char key or a key name. + if (!match(m, WORD_PAT)) + parseError("Expected key, for example \":str ...\".", m); + String key = m.group(0); + KeyValue k = KeyValue.getSpecialKeyByName(key); + if (k == null) + return KeyValue.makeStringKey(key); + return k; + } + + static KeyValue parseComplexKeyValue(Matcher m, String kind) throws ParseError + { // Attributes + String symbol = null; + int flags = 0; while (true) { if (!match(m, ATTR_PAT)) @@ -82,6 +116,14 @@ static public KeyValue parse(String str) throws ParseError symbol = String.valueOf(eventcode); return KeyValue.keyeventKey(symbol, eventcode, flags); + case "macro": + // :macro symbol='copy':ctrl,a,ctrl,c + // :macro symbol='acute':compose,' + KeyValue[] macro = parseKeyValueList(m); + if (symbol == null) + symbol = "macro"; + return KeyValue.makeMacro(symbol, macro, flags); + default: break; } parseError("Unknown kind '"+kind+"'", m, 1); @@ -117,6 +159,16 @@ static int parseFlags(String s, Matcher m) throws ParseError return flags; } + // Parse keys separated by comas + static KeyValue[] parseKeyValueList(Matcher m) throws ParseError + { + ArrayList out = new ArrayList(); + out.add(parseKeyValue(m)); + while (match(m, COMMA_PAT)) + out.add(parseKeyValue(m)); + return out.toArray(new KeyValue[]{}); + } + static boolean match(Matcher m, Pattern pat) { try { m.region(m.end(), m.regionEnd()); } catch (Exception _e) {} @@ -124,17 +176,6 @@ static boolean match(Matcher m, Pattern 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_]*"); - } - static void parseError(String msg, Matcher m) throws ParseError { parseError(msg, m, m.regionStart()); diff --git a/test/juloo.keyboard2/KeyValueParserTest.java b/test/juloo.keyboard2/KeyValueParserTest.java index a636ebf41..d1f467ba4 100644 --- a/test/juloo.keyboard2/KeyValueParserTest.java +++ b/test/juloo.keyboard2/KeyValueParserTest.java @@ -41,6 +41,32 @@ public void parseChar() throws Exception Utils.parse(":char:b", KeyValue.makeCharKey('b', "b", 0)); } + @Test + public void parseKeyValue() throws Exception + { + Utils.parse("\'", KeyValue.makeStringKey("\'")); + Utils.parse("a", KeyValue.makeStringKey("a")); + Utils.parse("abc", KeyValue.makeStringKey("abc")); + Utils.parse("shift", KeyValue.getSpecialKeyByName("shift")); + Utils.expect_error("\'a"); + } + + @Test + public void parseMacro() throws Exception + { + Utils.parse(":macro symbol='copy':ctrl,a,ctrl,c", KeyValue.makeMacro("copy", new KeyValue[]{ + KeyValue.getSpecialKeyByName("ctrl"), + KeyValue.makeStringKey("a"), + KeyValue.getSpecialKeyByName("ctrl"), + KeyValue.makeStringKey("c") + }, 0)); + Utils.parse(":macro:abc,'", KeyValue.makeMacro("macro", new KeyValue[]{ + KeyValue.makeStringKey("abc"), + KeyValue.makeStringKey("'") + }, 0)); + Utils.expect_error(":macro:"); + } + /** JUnit removes these functions from stacktraces. */ static class Utils {