From ca25cc55f6bc2c7b3100da2f7bf18a078a23f55e Mon Sep 17 00:00:00 2001 From: Jules Aguillon Date: Sun, 23 Feb 2025 18:00:44 +0100 Subject: [PATCH] Apply compose sequences to String keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is mostly useful for characters that do not fit on a single 16-bit char. Shift sequences for ๐•จ๐•ฉ๐•—๐•˜๐•ค are added for illustration. --- srcs/compose/shift.json | 11 ++++- srcs/juloo.keyboard2/ComposeKey.java | 42 +++++++++------- srcs/juloo.keyboard2/ComposeKeyData.java | 14 +++--- srcs/juloo.keyboard2/KeyModifier.java | 62 +++++++++++++++--------- srcs/juloo.keyboard2/LayoutModifier.java | 11 +---- test/juloo.keyboard2/ComposeKeyTest.java | 25 +++++----- 6 files changed, 95 insertions(+), 70 deletions(-) diff --git a/srcs/compose/shift.json b/srcs/compose/shift.json index 8b87006..0a44829 100644 --- a/srcs/compose/shift.json +++ b/srcs/compose/shift.json @@ -125,5 +125,14 @@ "เฅข": "เฅฃ", "เฅ’": "เฅ‘", "เฅ…": "เฅฒ", - "เฅ‰": "เค‘" + "เฅ‰": "เค‘", + + // Mathematical symbols + "\uD835": { + "\uDD68": "๐•Ž", // ๐•จ โ†’ ๐•Ž + "\uDD69": "๐•", // ๐•ฉ โ†’ ๐• + "\uDD57": "๐”ฝ", // ๐•— โ†’ ๐”ฝ + "\uDD58": "๐”พ", // ๐•˜ โ†’ ๐”พ + "\uDD64": "๐•Š" // ๐•ค โ†’ ๐•Š + } } diff --git a/srcs/juloo.keyboard2/ComposeKey.java b/srcs/juloo.keyboard2/ComposeKey.java index f2169b1..57b2a4e 100644 --- a/srcs/juloo.keyboard2/ComposeKey.java +++ b/srcs/juloo.keyboard2/ComposeKey.java @@ -4,31 +4,22 @@ import java.util.Arrays; public final class ComposeKey { - /** Apply the pending compose sequence to [kv]. */ + /** Apply the pending compose sequence to [kv]. Returns [null] if no sequence + matched. */ public static KeyValue apply(int state, KeyValue kv) { switch (kv.getKind()) { case Char: - KeyValue res = apply(state, kv.getChar()); - // Grey-out characters not part of any sequence. - if (res == null) - return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED); - return res; - /* Tapping compose again exits the pending sequence. */ - case Compose_pending: - return KeyValue.getKeyByName("compose_cancel"); - /* These keys are not greyed. */ - case Event: - case Modifier: - return kv; - /* Other keys cannot be part of sequences. */ - default: - return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED); + return apply(state, kv.getChar()); + case String: + return apply(state, kv.getString()); } + return null; } - /** Apply the pending compose sequence to char [c]. */ + /** Apply the pending compose sequence to char [c]. Returns [null] if no + sequence matched. */ public static KeyValue apply(int prev, char c) { char[] states = ComposeKeyData.states; @@ -51,6 +42,23 @@ public final class ComposeKey return KeyValue.makeCharKey((char)next_header); } + /** Apply each char of a string to a sequence. Returns [null] if no sequence + matched. */ + public static KeyValue apply(int prev, String s) + { + int i = 0; + while (true) + { + KeyValue k = apply(prev, s.charAt(i)); + i++; + if (k == null) return null; + if (i >= s.length()) return k; + if (k.getKind() != KeyValue.Kind.Compose_pending) + return null; // Found a final state before the end of [s]. + prev = k.getPendingCompose(); + } + } + /** The state machine is comprised of two arrays. The [states] array represents the different states and the associated diff --git a/srcs/juloo.keyboard2/ComposeKeyData.java b/srcs/juloo.keyboard2/ComposeKeyData.java index 2c8b124..f84397a 100644 --- a/srcs/juloo.keyboard2/ComposeKeyData.java +++ b/srcs/juloo.keyboard2/ComposeKeyData.java @@ -118,9 +118,9 @@ public final class ComposeKeyData "\u0963\u0965\u0971\uFFFF\u0bd0\uFFFF\u0bf2\uFFFF\u0bf0\uFFFF\u0bf1\uFFFF\u0bf3\u21d4\u21d5\u21d6\u21d7\u21d8\u21d9\u22c0\u22c1\u22c2\u22c3\u222e\u22b6\u044b\u0483\u00000123456789\u09e6\u09e7\u09e8\u09e9\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u00000123456789\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u000001" + "23456789\u0ae6\u0ae7\u0ae8\u0ae9\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef\u00000123456789\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u00000123456789\u0ce6\u0ce7\u0ce8\u0ce9\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u00000123456789\u06f0" + "\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\u00000123456789\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0000\u00df\u0131\u01f0\u0237\u02b0\u02b2\u02b3\u02b7\u02e1\u0905\u0907\u0909\u090b\u090c\u090f\u0913\u0915\u0917\u091a\u091c\u091f\u0921\u0924\u0926\u0928\u092c\u092e\u0932\u0938\u0939\u093f\u0941\u0943\u0945\u0947\u0949\u094b\u0952\u0962\u0a85\u0a87" + - "\u0a89\u0a8f\u0a93\u0a95\u0a97\u0a9a\u0a9c\u0a9f\u0aa1\u0aa4\u0aa6\u0aa8\u0aaa\u0aac\u0aae\u0ab2\u0ab8\u0ab9\u0abf\u0ac1\u0ac7\u0acb\u0bf9\u1d43\u1d47\u1d48\u1d49\u1d4d\u1d4f\u1d50\u1d52\u1d56\u1d57\u1d58\u1d5b\u1d60\u1d9c\u1da0\u1dbe\u1e97\u1e98\u1e99\u2071\u207f\u20b9\u2190\u2191\u2192\u2193\u2196\u2197\u2198\u2199\u2208\u220b\u2282\u2283\u2286\u2287\u2500\u2502\u250c\u2510\u2514\u2518\u251c\u2524\u252c\u2534\u253c\uFFFF\u004a" + - "\u030c\uFFFF\u004a\u0307\u1d34\u1d36\u1d3f\u1d42\u1d38\u0906\u0908\u090a\u0910\u0914\u0916\u0918\u091b\u091d\u0920\u0922\u0925\u0927\u0923\u092d\u0902\u0933\u0936\u0903\u0940\u0942\u0948\u094c\u0951\u0a86\u0a88\u0a8a\u0a90\u0a94\u0a96\u0a98\u0a9b\u0a9d\u0aa0\u0aa2\u0aa5\u0aa7\u0aa3\u0aab\u0aad\u0a82\u0ab3\u0ab6\u0a83\u0ac0\u0ac2\u0ac8\u0acc\u1d2c\u1d2e\u1d30\u1d31\u1d33\u1d37\u1d39\u1d3c\u1d3e\u1d40\u1d41\u2c7d\u1db2\uFFFF\ua7f2" + - "\uFFFF\ua7f3\u1d23\uFFFF\u0054\u0308\uFFFF\u0057\u030a\uFFFF\u0059\u030a\u1d35\u1d3a\u2550\u2551\u2554\u2557\u255a\u255d\u2560\u2563\u2566\u2569\u256c").toCharArray(); + "\u0a89\u0a8f\u0a93\u0a95\u0a97\u0a9a\u0a9c\u0a9f\u0aa1\u0aa4\u0aa6\u0aa8\u0aaa\u0aac\u0aae\u0ab2\u0ab8\u0ab9\u0abf\u0ac1\u0ac7\u0acb\u0bf9\u1d43\u1d47\u1d48\u1d49\u1d4d\u1d4f\u1d50\u1d52\u1d56\u1d57\u1d58\u1d5b\u1d60\u1d9c\u1da0\u1dbe\u1e97\u1e98\u1e99\u2071\u207f\u20b9\u2190\u2191\u2192\u2193\u2196\u2197\u2198\u2199\u2208\u220b\u2282\u2283\u2286\u2287\u2500\u2502\u250c\u2510\u2514\u2518\u251c\u2524\u252c\u2534\u253c\ud835\uFFFF" + + "\u004a\u030c\uFFFF\u004a\u0307\u1d34\u1d36\u1d3f\u1d42\u1d38\u0906\u0908\u090a\u0910\u0914\u0916\u0918\u091b\u091d\u0920\u0922\u0925\u0927\u0923\u092d\u0902\u0933\u0936\u0903\u0940\u0942\u0948\u094c\u0951\u0a86\u0a88\u0a8a\u0a90\u0a94\u0a96\u0a98\u0a9b\u0a9d\u0aa0\u0aa2\u0aa5\u0aa7\u0aa3\u0aab\u0aad\u0a82\u0ab3\u0ab6\u0a83\u0ac0\u0ac2\u0ac8\u0acc\u1d2c\u1d2e\u1d30\u1d31\u1d33\u1d37\u1d39\u1d3c\u1d3e\u1d40\u1d41\u2c7d\u1db2\uFFFF" + + "\ua7f2\uFFFF\ua7f3\u1d23\uFFFF\u0054\u0308\uFFFF\u0057\u030a\uFFFF\u0059\u030a\u1d35\u1d3a\u2550\u2551\u2554\u2557\u255a\u255d\u2560\u2563\u2566\u2569\u256c\u0000\udd57\udd58\udd64\udd68\udd69\uFFFF\ud835\udd3d\uFFFF\ud835\udd3e\uFFFF\ud835\udd4a\uFFFF\ud835\udd4e\uFFFF\ud835\udd4f").toCharArray(); public static final char[] edges = ("\u0001\u0036\u0037\u0038\u0039\u003a\u003b\u003c\u003f\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004a\u004b\u004c\u004d\u004e\u004f\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0059\u005a\u005b\\\u005d\u005e\u005f\u0060\u0061\u0062\u0063\u0064\u0067\u0068\u006b\u006e\u006f\u0072\u0075\u0078\u007b\u007e\u0081\u0001\u0001\u0001\u0001\u0001\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" + @@ -235,10 +235,10 @@ public final class ComposeKeyData "\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0004\u0000\u0000\u0000\u0001\u0001\u0001\u0004\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0001\u0001\u0004\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001" + "\u0001\u0001\u0001\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0002\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f16\u1f17\u1f18\u1f19\u1f1a\u1f1b\u1f1c\u1f1d\u1f1e\u1f1f\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f2b\u1f2c\u1f2d\u1f2e\u1f2f\u1f30\u1f31\u1f32\u1f33\u1f34\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f40\u1f41" + "\u1f42\u1f43\u1f44\u1f45\u1f46\u1f47\u1f48\u1f49\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f55\u1f56\u1f57\u1f58\u1f59\u1f5a\u1f5b\u1f5c\u1f5d\u1f5e\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f6a\u1f6b\u1f6c\u1f6d\u1f6e\u1f6f\u1f70\u1f71\u1f72\u1f73\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f7f\u1f80\u1f81\u1f82\u1f83\u1f84\u1f85\u1f86\u1f87\u1f88\u0001" + - "\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f94\u1f95\u1f96\u1f97\u1f98\u1f99\u1f9a\u1f9b\u1f9c\u1f9d\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0070\u0f55\u0d0b\u200e\u2011\u2014\u2015\u2016\u2017\u2018\u2019\u201a\u201b\u1ebe\u1ebf\u201c\u201d\u201e\u201f\u2020\u2021\u2022\u2023\u2024\u2025\u2026\u2027\u2028\u2029\u202a\u202b\u202c\u202d\u1ee9\u1eb8\u202e\u1eb9\u202f\u2030\u1ef0\u2031\u2032" + - "\u2033\u2034\u2035\u2036\u2037\u2038\u2039\u203a\u203b\u203c\u203d\u203e\u203f\u2040\u2041\u2042\u2043\u2044\u2045\u2046\u2047\u2048\u0f48\u2049\u204a\u204b\u204c\u204d\u204e\u204f\u2050\u2051\u2052\u2053\u2054\u2055\u2056\u2058\u205a\u205b\u205e\u2061\u2064\u2065\u0f48\u0e0d\u0e19\u0e0e\u0e1c\u1eff\u1f00\u1f01\u1f02\u1c43\u1c48\u1c9e\u1ca2\u1ca5\u1ca8\u2066\u2067\u2068\u2069\u206a\u206b\u206c\u206d\u206e\u206f\u2070\u0003\u0000" + - "\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0000" + - "\u0002\u0000\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001").toCharArray(); + "\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u000b\u1f94\u1f95\u1f96\u1f97\u1f98\u1f99\u1f9a\u1f9b\u1f9c\u1f9d\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0071\u0f55\u0d0b\u200f\u2012\u2015\u2016\u2017\u2018\u2019\u201a\u201b\u201c\u1ebe\u1ebf\u201d\u201e\u201f\u2020\u2021\u2022\u2023\u2024\u2025\u2026\u2027\u2028\u2029\u202a\u202b\u202c\u202d\u202e\u1ee9\u1eb8\u202f\u1eb9\u2030\u2031\u1ef0\u2032\u2033" + + "\u2034\u2035\u2036\u2037\u2038\u2039\u203a\u203b\u203c\u203d\u203e\u203f\u2040\u2041\u2042\u2043\u2044\u2045\u2046\u2047\u2048\u2049\u0f48\u204a\u204b\u204c\u204d\u204e\u204f\u2050\u2051\u2052\u2053\u2054\u2055\u2056\u2057\u2059\u205b\u205c\u205f\u2062\u2065\u2066\u0f48\u0e0d\u0e19\u0e0e\u0e1c\u1eff\u1f00\u1f01\u1f02\u1c43\u1c48\u1c9e\u1ca2\u1ca5\u1ca8\u2067\u2068\u2069\u206a\u206b\u206c\u206d\u206e\u206f\u2070\u2071\u2072\u0003" + + "\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002" + + "\u0000\u0002\u0000\u0001\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0006\u2078\u207b\u207e\u2081\u2084\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000\u0003\u0000\u0000").toCharArray(); public static final int accent_aigu = 1; public static final int accent_arrows = 130; diff --git a/srcs/juloo.keyboard2/KeyModifier.java b/srcs/juloo.keyboard2/KeyModifier.java index fcb0793..c9de407 100644 --- a/srcs/juloo.keyboard2/KeyModifier.java +++ b/srcs/juloo.keyboard2/KeyModifier.java @@ -36,7 +36,7 @@ public final class KeyModifier case Modifier: return modify(k, mod.getModifier()); case Compose_pending: - return ComposeKey.apply(mod.getPendingCompose(), k); + return apply_compose_pending(mod.getPendingCompose(), k); case Hangul_initial: if (k.equals(mod)) // Allow typing the initial in letter form return KeyValue.makeStringKey(k.getString(), KeyValue.FLAG_GREYED); @@ -122,30 +122,44 @@ public final class KeyModifier } } + /** Keys that do not match any sequence are greyed. */ + private static KeyValue apply_compose_pending(int state, KeyValue kv) + { + switch (kv.getKind()) + { + case Char: + case String: + KeyValue res = ComposeKey.apply(state, kv); + // Grey-out characters not part of any sequence. + if (res == null) + return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED); + return res; + /* Tapping compose again exits the pending sequence. */ + case Compose_pending: + return KeyValue.getKeyByName("compose_cancel"); + /* These keys are not greyed. */ + case Event: + case Modifier: + return kv; + /* Other keys cannot be part of sequences. */ + default: + return kv.withFlags(kv.getFlags() | KeyValue.FLAG_GREYED); + } + } + /** Apply the given compose state or fallback to the dead_char. */ private static KeyValue apply_compose_or_dead_char(KeyValue k, int state, char dead_char) { - switch (k.getKind()) - { - case Char: - char c = k.getChar(); - KeyValue r = ComposeKey.apply(state, c); - if (r != null) - return r; - } + KeyValue r = ComposeKey.apply(state, k); + if (r != null) + return r; return apply_dead_char(k, dead_char); } private static KeyValue apply_compose(KeyValue k, int state) { - switch (k.getKind()) - { - case Char: - KeyValue r = ComposeKey.apply(state, k.getChar()); - if (r != null) - return r; - } - return k; + KeyValue r = ComposeKey.apply(state, k); + return (r != null) ? r : k; } private static KeyValue apply_dead_char(KeyValue k, char dead_char) @@ -179,18 +193,19 @@ public final class KeyModifier if (mapped != null) return mapped; } + KeyValue r = ComposeKey.apply(ComposeKeyData.shift, k); + if (r != null) + return r; switch (k.getKind()) { case Char: char kc = k.getChar(); - KeyValue r = ComposeKey.apply(ComposeKeyData.shift, kc); - if (r != null) - return r; char c = Character.toUpperCase(kc); return (kc == c) ? k : k.withChar(c); case String: - String s = Utils.capitalize_string(k.getString()); - return KeyValue.makeStringKey(s, k.getFlags()); + String ks = k.getString(); + String s = Utils.capitalize_string(ks); + return s.equals(ks) ? k : KeyValue.makeStringKey(s, k.getFlags()); default: return k; } } @@ -207,7 +222,8 @@ public final class KeyModifier switch (k.getKind()) { case Char: - KeyValue r = ComposeKey.apply(ComposeKeyData.fn, k.getChar()); + case String: + KeyValue r = ComposeKey.apply(ComposeKeyData.fn, k); return (r != null) ? r : k; case Keyevent: name = apply_fn_keyevent(k.getKeyevent()); break; case Event: name = apply_fn_event(k.getEvent()); break; diff --git a/srcs/juloo.keyboard2/LayoutModifier.java b/srcs/juloo.keyboard2/LayoutModifier.java index 830a50c..7be3fb6 100644 --- a/srcs/juloo.keyboard2/LayoutModifier.java +++ b/srcs/juloo.keyboard2/LayoutModifier.java @@ -138,15 +138,8 @@ public final class LayoutModifier return new KeyboardData.MapKeyValues() { public KeyValue apply(KeyValue key, boolean localized) { - switch (key.getKind()) - { - case Char: - KeyValue modified = ComposeKey.apply(map_digit, key.getChar()); - if (modified != null) - return modified; - break; - } - return key; + KeyValue modified = ComposeKey.apply(map_digit, key); + return (modified != null) ? modified : key; } }; } diff --git a/test/juloo.keyboard2/ComposeKeyTest.java b/test/juloo.keyboard2/ComposeKeyTest.java index 3d0b371..a3d1ddb 100644 --- a/test/juloo.keyboard2/ComposeKeyTest.java +++ b/test/juloo.keyboard2/ComposeKeyTest.java @@ -42,22 +42,21 @@ public class ComposeKeyTest assertEquals(apply("เฎฏ", state), KeyValue.makeStringKey("เฏฐ", KeyValue.FLAG_SMALLER_FONT)); } - KeyValue apply(String seq) throws Exception + @Test + public void stringKeys() throws Exception { - return apply(seq, ComposeKeyData.compose); + int state = ComposeKeyData.shift; + assertEquals(apply("๐•จ", state), KeyValue.makeStringKey("๐•Ž")); + assertEquals(apply("๐•ฉ", state), KeyValue.makeStringKey("๐•")); } - KeyValue apply(String seq, int state) throws Exception + KeyValue apply(String seq) { - KeyValue r = null; - for (int i = 0; i < seq.length(); i++) - { - r = ComposeKey.apply(state, seq.charAt(i)); - if (r.getKind() == KeyValue.Kind.Compose_pending) - state = r.getPendingCompose(); - else if (i + 1 < seq.length()) - throw new Exception("Sequence too long: " + seq); - } - return r; + return ComposeKey.apply(ComposeKeyData.compose, seq); + } + + KeyValue apply(String seq, int state) + { + return ComposeKey.apply(state, seq); } }