From 77c4a27c4c37b3620defcab94ffd1b2f536c88cb Mon Sep 17 00:00:00 2001 From: Jules Aguillon Date: Mon, 2 Feb 2026 00:20:00 +0100 Subject: Spell checking (#1137) This adds dictionary-based spell checking to the keyboard. The keyboard looks at the word being typed and matches it against a dictionary to either complete the rest of the word or find alternative spellings. The core of this feature is implemented in cdict, which is included as a submodule in vendor/cidct. Cdict is developped at https://github.com/Julow/cdict The dictionaries are hosted at https://github.com/Julow/Unexpected-Keyboard-dictionaries/ The wordlists used to build the dictionaries are the same ones used by HeliBoard from https://codeberg.org/Helium314/aosp-dictionaries - Add an activity accessible from the launcher app that lists available dictionaries with a download button. The DictionaryListView view shows the list of available dictionaries and handles downloading and installing them. - The Dictionaries class manages installed dictionaries. Dictionaries are installed as individual files into the app's private directory. - Available dictionaries are listed in dictionaries.xml, which is generated when building Unexpected-Keyboard-dictionaries. method.xml mentions the dictionary name for each locales. --- .github/workflows/make-apk.yml | 4 +- .gitmodules | 3 + AndroidManifest.xml | 8 + build.gradle.kts | 9 +- gen_method_xml.py | 63 ++++-- res/drawable/ic_delete.xml | 2 +- res/drawable/ic_download.xml | 2 + res/layout/candidates_status_no_dict.xml | 2 +- res/layout/dictionaries_activity.xml | 7 + res/layout/dictionary_download_item.xml | 6 + res/layout/launcher_activity.xml | 1 + res/values/dictionaries.xml | 237 +++++++++++++++++++++ res/values/strings.xml | 4 + res/values/styles.xml | 33 ++- res/xml/method.xml | 134 ++++++------ shell.nix | 2 + srcs/juloo.keyboard2/Config.java | 18 +- srcs/juloo.keyboard2/DeviceLocales.java | 2 + srcs/juloo.keyboard2/KeyEventHandler.java | 4 +- srcs/juloo.keyboard2/Keyboard2.java | 45 +++- srcs/juloo.keyboard2/LauncherActivity.java | 7 + srcs/juloo.keyboard2/Utils.java | 12 ++ srcs/juloo.keyboard2/dict/Dictionaries.java | 149 +++++++++++++ .../juloo.keyboard2/dict/DictionariesActivity.java | 15 ++ srcs/juloo.keyboard2/dict/DictionaryListView.java | 191 +++++++++++++++++ .../dict/SupportedDictionaries.java | 33 +++ .../suggestions/CandidatesView.java | 2 +- srcs/juloo.keyboard2/suggestions/Suggestions.java | 27 ++- vendor/Android.mk | 12 ++ vendor/cdict | 1 + 30 files changed, 926 insertions(+), 109 deletions(-) create mode 100644 .gitmodules create mode 100644 res/drawable/ic_download.xml create mode 100644 res/layout/dictionaries_activity.xml create mode 100644 res/layout/dictionary_download_item.xml create mode 100644 res/values/dictionaries.xml create mode 100644 srcs/juloo.keyboard2/dict/Dictionaries.java create mode 100644 srcs/juloo.keyboard2/dict/DictionariesActivity.java create mode 100644 srcs/juloo.keyboard2/dict/DictionaryListView.java create mode 100644 srcs/juloo.keyboard2/dict/SupportedDictionaries.java create mode 100644 vendor/Android.mk create mode 160000 vendor/cdict diff --git a/.github/workflows/make-apk.yml b/.github/workflows/make-apk.yml index c7c7c5b..26fd7fd 100644 --- a/.github/workflows/make-apk.yml +++ b/.github/workflows/make-apk.yml @@ -10,7 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + submodules: true - name: Restore debug keystore from GitHub Secrets run: | # Check if exist and use the secret named DEBUG_KEYSTORE diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..364e005 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/cdict"] + path = vendor/cdict + url = https://github.com/Julow/cdict diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fb47a5f..95d80b9 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,6 +2,8 @@ + + @@ -23,6 +25,12 @@ + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index e111ac8..782b8db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,7 +27,7 @@ android { sourceSets { named("main") { manifest.srcFile("AndroidManifest.xml") - java.srcDirs("srcs/juloo.keyboard2") + java.srcDirs("srcs/juloo.keyboard2", "vendor/cdict/java/juloo.cdict") res.srcDirs("res", "build/generated-resources") assets.srcDirs("assets") } @@ -37,6 +37,12 @@ android { } } + externalNativeBuild { + ndkBuild { + path = file("vendor/Android.mk") + } + } + signingConfigs { // Debug builds will always be signed. If no environment variables are set, a default // keystore will be initialized by the task initDebugKeystore and used. This keystore @@ -116,6 +122,7 @@ val genLayoutsList by tasks.registering(Exec::class) { val genMethodXml by tasks.registering(Exec::class) { val out = projectDir.resolve("res/xml/method.xml") inputs.file(projectDir.resolve("gen_method_xml.py")) + inputs.file(projectDir.resolve("res/values/dictionaries.xml")) outputs.file(out) doFirst { println("\nGenerating res/xml/method.xml") } doFirst { standardOutput = FileOutputStream(out) } diff --git a/gen_method_xml.py b/gen_method_xml.py index 3b1962f..c01f212 100644 --- a/gen_method_xml.py +++ b/gen_method_xml.py @@ -1,3 +1,4 @@ +import xml.etree.ElementTree as ET import itertools as it # This script generates res/xml/method.xml. @@ -78,37 +79,69 @@ LOCALES = [ ] # The locale that is at the beginning of the list -DEFAULT_LOCALE = loc("en", "latin", "latn_qwerty_us", tag="en") +DEFAULT_LOCALE = loc("en", "latin", "latn_qwerty_us", tag="en", dictionary="en_US") -def loc_to_subtype(loc): +def parse_dictionaries(): + tree = ET.parse("res/values/dictionaries.xml") + root = tree.getroot() + return set(( it.text for it in root.findall('*[@name="dictionaries_locale"]/item') )) + +# Available dictionares of the form "de" or "de_CH". +available_dictionaries = parse_dictionaries() + +def subtype_elem(root, loc): tag = loc["tag"].replace("_", "-") extra_keys = ",extra_keys=" + loc["extra_keys"] if "extra_keys" in loc else "" - return f'' + dictionaries = ",dictionary=" + loc["dictionary"] if loc["dictionary"] != None else "" + extra_value = f'script={loc["script"]},default_layout={loc["default_layout"]}{dictionaries}{extra_keys}' + ET.SubElement(root, "subtype", attrib={ + "android:label": "%s", + "android:languageTag": tag, + "android:imeSubtypeLocale": loc["name"], + "android:imeSubtypeMode": "keyboard", + "android:isAsciiCapable": "true", + "android:imeSubtypeExtraValue": extra_value + }) -# Return locales in sorted order with the 'tag' item added. -def compute_tags(): +# Return locales in sorted order with the "tag" and "dictionary" attributes +# added. +def compute_attrs(): + locales_grouped = {} # Locales grouped by language tag def lang(loc): return loc["name"].split("_")[0] - locales_grouped = { k: list(v) for k, v in it.groupby(sorted(LOCALES, key=lang), lang) } + for loc in LOCALES: + locales_grouped.setdefault(lang(loc), []).append(loc) def tag(loc): if "tag" in loc: return loc["tag"] l = lang(loc) if loc["name"] == f"{l}_{l.upper()}": return l # Locales like "fr_FR" # Return a short tag when it's not shared between several locales return l if len(locales_grouped[l]) == 1 else loc["name"] - return [ dict(tag=tag(loc), **loc) for loc in LOCALES ] + def dictionary(loc): + if loc["name"] in available_dictionaries: return loc["name"] + l = lang(loc) + if l in available_dictionaries: return l + return None + def add_attrs(loc): + return dict(tag=tag(loc), dictionary=dictionary(loc), **loc) + return map(add_attrs, LOCALES) def gen(): - locales = compute_tags() - print(f""" - - - {loc_to_subtype(DEFAULT_LOCALE)} - {"\n ".join(sorted(map(loc_to_subtype, locales)))} -""") + """)) + subtype_elem(root, DEFAULT_LOCALE) + for loc in sorted(locales, key=lambda loc: loc["name"]): + subtype_elem(root, loc) + ET.indent(root) + print(ET.tostring(root, encoding="utf-8", xml_declaration=True).decode("UTF-8")) gen() diff --git a/res/drawable/ic_delete.xml b/res/drawable/ic_delete.xml index 5b69d0b..08a5f71 100644 --- a/res/drawable/ic_delete.xml +++ b/res/drawable/ic_delete.xml @@ -1 +1 @@ - \ No newline at end of file + diff --git a/res/drawable/ic_download.xml b/res/drawable/ic_download.xml new file mode 100644 index 0000000..1fb7e98 --- /dev/null +++ b/res/drawable/ic_download.xml @@ -0,0 +1,2 @@ + + diff --git a/res/layout/candidates_status_no_dict.xml b/res/layout/candidates_status_no_dict.xml index 6ba20b9..8bd1e37 100644 --- a/res/layout/candidates_status_no_dict.xml +++ b/res/layout/candidates_status_no_dict.xml @@ -1,5 +1,5 @@ -