Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15f93cade1 | ||
|
|
345f3cdbd6 | ||
|
|
7aaebb5bfd | ||
|
|
e17b7db68a | ||
|
|
9bdfaa56e6 | ||
|
|
eb9a5103c2 | ||
|
|
f59603c90b | ||
|
|
4534df8bcb | ||
|
|
ecdb43a593 | ||
|
|
939e8da252 | ||
|
|
e2d5b47c7c | ||
|
|
96966332ce | ||
|
|
b034191a1c | ||
|
|
b86481461a | ||
|
|
5fbd21c7d9 | ||
|
|
b6618444a2 | ||
|
|
4a05bf54d5 | ||
|
|
9c70f1113e | ||
|
|
c0a4bca8e2 | ||
|
|
b6ba00a1f6 | ||
|
|
43550a3ad1 | ||
|
|
b73ac529e8 | ||
|
|
f192dfcb32 | ||
|
|
4ab921f112 | ||
|
|
44dcf6e753 | ||
|
|
ccd9568e3d | ||
|
|
21633f22fd | ||
|
|
c6a568a74b | ||
|
|
241e034a3f | ||
|
|
27329091af | ||
|
|
a7645c3934 | ||
|
|
374c47af9d | ||
|
|
49627b63e3 | ||
|
|
2a14ec777c | ||
|
|
6b6491c47a | ||
|
|
dbe09902d4 | ||
|
+3 |
ff769489c3 | ||
|
|
bfdb1a9604 | ||
|
|
0eb0e6b76f | ||
|
|
76ee5b2134 | ||
|
|
b81f4acac9 | ||
|
|
7e19b1f68e | ||
|
|
e64da8eefe | ||
|
|
8532acf814 | ||
|
|
aa11bb25fd | ||
|
|
779d438fa0 | ||
|
|
b807f217a0 | ||
|
|
9d0a849d34 | ||
|
|
267894b5ff | ||
|
|
56edba7b18 | ||
|
|
11b735bfe8 | ||
|
|
7587c68d7f | ||
|
|
e57cdc45f2 | ||
|
|
7b07b14a80 | ||
|
|
063756eb50 | ||
|
|
9189921719 | ||
|
|
2e1859fb1a | ||
|
|
139d1c5691 | ||
|
|
20a4916ff6 | ||
|
|
54c25b79e4 | ||
|
|
82d61d4406 | ||
|
|
e2fdced765 | ||
|
|
78a7ffc473 | ||
|
+34 |
467d7e83a5 | ||
|
|
5d9a105e5d | ||
|
|
9c23e6c5f3 | ||
|
|
d164820bca | ||
|
|
b844af1f28 | ||
|
|
6477c90506 | ||
|
|
c6774196ea | ||
|
|
af5f6df02c | ||
|
|
08eb3a04a6 | ||
|
|
e832c3cd80 | ||
|
|
a1d0342033 | ||
|
|
5ec663c306 | ||
|
|
833b4a2154 | ||
|
|
8921de7f02 | ||
|
|
afa7c0187a | ||
|
|
7bd9306a9f | ||
|
|
f29ae92ba7 | ||
|
|
5f63214c08 | ||
|
|
0f3ec187c9 | ||
|
|
13f1996af9 | ||
|
|
35ccaafe65 | ||
|
|
963399417e | ||
|
|
4dab3a609c | ||
|
|
2966a45922 | ||
|
|
b25b670c6b | ||
|
|
ece8fb51ce | ||
|
|
18157a1cd8 | ||
|
|
27fdcb6bfc | ||
|
|
6a19f7ccd3 | ||
|
|
164c8732c0 | ||
|
|
958900ca2f | ||
|
|
ae4a9d4514 | ||
|
|
91b2970c1f | ||
|
|
9863dcea9e | ||
|
|
a80afa5fbe | ||
|
|
d172455eb4 | ||
|
|
df3594bd53 | ||
|
|
ebf80415d8 | ||
|
|
1cd252f8fa | ||
|
|
0109f30e22 | ||
|
|
77c4a27c4c | ||
|
|
2ecf93d990 | ||
|
|
b9072daaf6 | ||
|
|
1b8506876a | ||
|
|
e05be0821b | ||
|
|
c669a755a6 | ||
|
|
3b4aa33ff3 | ||
|
|
a848897586 | ||
|
|
4d4c22b4e2 | ||
|
|
bdb05bcef0 | ||
|
|
2208079e48 | ||
|
|
c4d802d873 | ||
|
|
2b60f94ead | ||
|
|
f082fcdebc | ||
|
|
98c1b8db82 | ||
|
|
dfaf4dbb57 | ||
|
|
dab50e0572 | ||
|
|
b40be68773 | ||
|
|
258215f26c | ||
|
|
41777fdda6 | ||
|
|
be0aa07a2a | ||
|
|
5fc1da0a2e | ||
|
|
f834b2a82a | ||
|
|
498194231f | ||
|
|
08ebf8fabc | ||
|
|
0ad03c6dea | ||
|
|
be8a99bdc6 | ||
|
|
00cf534e00 | ||
|
|
c717461ef5 | ||
|
|
eae05bb9bd | ||
|
|
ac498d4ee9 | ||
|
|
02c11132b2 | ||
|
|
635c7d5882 | ||
|
|
df8e7a01e0 | ||
|
|
7fdf63b930 | ||
|
|
462b860e4e | ||
|
|
8db00d51ab | ||
|
|
ab85fb717d | ||
|
|
5b3dc8d5ed | ||
|
|
795889bf81 | ||
|
|
6d0aa4cf51 | ||
|
|
dfb2c7eaf9 | ||
|
|
cedf3aeb1c | ||
|
|
c1d473b4a0 | ||
|
|
00d7d88271 | ||
|
|
7e4e4bf4d9 | ||
|
|
020e80019b | ||
|
|
679f52cf45 | ||
|
|
9588f55619 | ||
|
|
5c7484388f | ||
|
|
2bed42857b | ||
|
|
fad74bb123 | ||
|
|
43dd3a2818 | ||
|
|
c4a1e2e664 | ||
|
|
13acf90541 | ||
|
|
a83b40066e | ||
|
|
c6e35ba4b6 | ||
|
|
e013128142 | ||
|
|
898956256a | ||
|
|
8cb9b30461 | ||
|
|
309fba4766 | ||
|
|
d1dfc80df6 | ||
|
|
5c4964e438 | ||
|
|
957dbdc7fe | ||
|
|
babc3ec88a | ||
|
|
42fa47459c | ||
|
|
30d1f5635c | ||
|
+9 |
75c1ce38df | ||
|
|
c308b57611 | ||
|
|
52c0e8c798 | ||
|
|
1ff3bf2fc9 | ||
|
|
a08db25661 | ||
|
|
4ed8594794 | ||
|
|
f36a7c8e9d | ||
|
|
004ed95dfd | ||
|
|
9d12981b34 | ||
|
|
c6da6d6ab5 | ||
|
|
2e0be0b32b | ||
|
|
42528ab211 | ||
|
|
71d2784026 | ||
|
|
f9b844e988 | ||
|
|
6f0244a29e | ||
|
|
2a50a4a129 | ||
|
|
9ab099175d | ||
|
|
0445c310ad | ||
|
|
536b8d1f67 | ||
|
|
d36452d9c3 | ||
|
|
97355881a8 | ||
|
|
a7312054b5 | ||
|
|
3b94bf6a17 | ||
|
|
ff9d2e7d31 | ||
|
|
b670fe0da2 | ||
|
|
32e82d6791 | ||
|
|
de5c5b5b60 | ||
|
|
d6dfb5c18c | ||
|
|
460e5214a2 | ||
|
|
5ae3ec05e7 | ||
|
|
70fb1e8fc1 | ||
|
|
2bd3e107ac | ||
|
|
4ae45bd00f | ||
|
|
7a6aef3360 | ||
|
|
9fac081ab8 | ||
|
|
b4e8b10e2b | ||
|
|
a7d73ebd8a | ||
|
|
0acf66d455 | ||
|
|
85c73d9f62 | ||
|
|
58dabfaa07 | ||
|
|
3d7925340b | ||
|
|
e746ccd5d0 | ||
|
|
6541054cce | ||
|
|
3971f6243c | ||
|
|
6c40e0604a | ||
|
|
1d6a115ce4 | ||
|
|
6717613b5e | ||
|
|
35219a458a | ||
|
|
f0ebe45439 | ||
|
|
0ef36d52e1 | ||
|
|
08860eed17 | ||
|
|
b8bd3f7b0f | ||
|
|
60b01927ab | ||
|
|
9cfeb0f0c2 | ||
|
|
5e77fa84cf | ||
|
|
06fbc83c9c | ||
|
|
a123810247 | ||
|
|
906755b787 | ||
|
|
80c52460c7 | ||
|
|
2b978dd2e0 | ||
|
|
55cb5f5315 | ||
|
|
b7d1508d7b | ||
|
|
77b0c30728 | ||
|
|
466e0b8218 | ||
|
|
75fdc2bfa9 | ||
|
|
bee59aceaa | ||
|
|
92d50dd73f | ||
|
|
ba05b2770e | ||
|
|
44d1343b83 | ||
|
|
ca25cc55f6 | ||
|
|
68be82a4f9 | ||
|
|
581b31bf99 | ||
|
|
56903084f2 | ||
|
|
ac8fff599f | ||
|
|
a22cc39a45 | ||
|
|
7e86ca70cd | ||
|
|
33f8bf9dea | ||
|
|
653c598a1c | ||
|
|
bd5c815a6f | ||
|
|
06b76d58c2 | ||
|
|
255d7f80d7 | ||
|
|
d3753cc455 | ||
|
|
96dabde7c8 | ||
|
|
bd1a72d01e | ||
|
|
87667a2438 | ||
|
|
8f729bb90a | ||
|
|
aaf0a9a249 | ||
|
|
ae9c2fa626 | ||
|
|
0061911ef3 | ||
|
|
6b40803fa4 | ||
|
|
defc4bc0bd | ||
|
|
e1191f3add | ||
|
|
188c68279d | ||
|
|
3e3fd6ffda | ||
|
|
81c493bff2 | ||
|
|
e97dbbc771 | ||
|
|
1f9e450dfa | ||
|
|
b93bc90242 | ||
|
|
c6908ec846 | ||
|
|
16c2be96b3 | ||
|
|
d4a6bfe122 | ||
|
|
e3f9341ed1 | ||
|
|
3ea5c8d6b7 | ||
|
|
f7c5b74940 | ||
|
|
4351b3eb6e | ||
|
|
bffc76907a | ||
|
|
ca05c073d2 | ||
|
|
ddd2eebb0e | ||
|
|
1783dcdb35 | ||
|
|
4f8b5fa6ce | ||
|
|
c4e2b446e5 | ||
|
|
7b8f739400 | ||
|
|
a3023a7f18 | ||
|
|
6ce2195253 | ||
|
|
9ba718694b | ||
|
|
11c9bd850b | ||
|
|
b120fa8f09 | ||
|
|
e8c20bf521 | ||
|
|
56c62a005d | ||
|
|
115eda4084 | ||
|
|
55060f656e | ||
|
|
42c23d3864 | ||
|
|
f64a0be6fa | ||
|
|
b43cbf77c2 | ||
|
|
568b5a99f6 | ||
|
|
12d9e5eaf8 | ||
|
|
e594915e80 | ||
|
|
78974b7453 | ||
|
|
1a4e61557f | ||
|
|
e13f58658d |
@@ -0,0 +1,39 @@
|
||||
name: Check generated files
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
gen-layouts:
|
||||
name: Layout list
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- run: python3 gen_layouts.py
|
||||
- name: "Run 'python3 gen_layouts.py' to update the generated files."
|
||||
run: git diff --exit-code
|
||||
|
||||
check-layouts:
|
||||
name: Layout warnings
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- run: python3 check_layout.py
|
||||
- name: "Fix your layout accordingly or run 'python3 check_layout.py' to promote the warnings"
|
||||
run: git diff --exit-code
|
||||
|
||||
check-compose:
|
||||
name: Compose key data
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- run: python3 srcs/compose/compile.py srcs/compose/{*.json,compose} > srcs/juloo.mornkeyboard2/ComposeKeyData.java
|
||||
- name: "Run 'gradle test' to update the generated files."
|
||||
run: git diff --exit-code
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Check layouts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
gen-layouts:
|
||||
name: Generated files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- run: python3 gen_layouts.py
|
||||
- name: "Check that the generated 'layouts.xml' is uptodate, otherwise run 'python3 gen_layouts.py'"
|
||||
run: git diff --exit-code
|
||||
check-layouts:
|
||||
name: check_layout.output
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- run: python3 check_layout.py
|
||||
- name: "Check that the generated 'check_layout.output' is uptodate, otherwise run 'python3 check_layout.py'"
|
||||
run: git diff --exit-code
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Check translations
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check-translations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- run: python3 sync_translations.py
|
||||
- name: "Check that strings files are uptodate, otherwise run 'python3 sync_translations.py'"
|
||||
run: git add -N . && git diff --exit-code
|
||||
@@ -3,18 +3,19 @@ name: Make Apk CI
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
Build-Apk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- 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
|
||||
@@ -28,15 +29,32 @@ jobs:
|
||||
gpg -d --passphrase "debug0" --batch "debug.keystore.asc" > "debug.keystore"
|
||||
fi
|
||||
fi
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
gradle-version: 8.14.4
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
gradle test
|
||||
# Warn about outdated generated files.
|
||||
if ! git diff --name-only; then
|
||||
echo "Warning: Generated files are not uptodate. Run 'gradle test'."
|
||||
fi
|
||||
|
||||
- name: Build debug APK
|
||||
uses: gradle/gradle-build-action@v3
|
||||
run: ./gradlew assembleDebug
|
||||
env:
|
||||
DEBUG_KEYSTORE: "debug.keystore"
|
||||
DEBUG_KEYSTORE_PASSWORD: debug0
|
||||
DEBUG_KEY_ALIAS: debug
|
||||
DEBUG_KEY_PASSWORD: debug0
|
||||
with:
|
||||
arguments: assembleDebug
|
||||
|
||||
- name: Artifact naming
|
||||
run: |
|
||||
artifact="${{github.repository_owner}} ${{github.ref_name}}"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
*.keystore
|
||||
*.keystore.asc
|
||||
/*-keystore.conf
|
||||
/build/
|
||||
*.apk
|
||||
*.ap_
|
||||
*.dex
|
||||
*.class
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/captures
|
||||
/build
|
||||
# Directory _build is not used anymore
|
||||
/_build
|
||||
.idea/
|
||||
.gradle/
|
||||
local.properties
|
||||
/libs/libswipetype/*/build/
|
||||
**/.cxx/
|
||||
debug.keystore
|
||||
build/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "vendor/cdict"]
|
||||
path = vendor/cdict
|
||||
url = https://github.com/Julow/cdict
|
||||
@@ -2,27 +2,35 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:hardwareAccelerated="false">
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<application android:label="@string/app_name" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:hardwareAccelerated="false">
|
||||
<service android:name="juloo.keyboard2.Keyboard2" android:label="@string/app_name" android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true" android:directBootAware="true">
|
||||
<service android:name="juloo.mornkeyboard2.Keyboard2" android:label="@string/app_name" android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true" android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.view.im" android:resource="@xml/method"/>
|
||||
</service>
|
||||
|
||||
<activity android:name="juloo.keyboard2.SettingsActivity" android:icon="@mipmap/ic_launcher" android:label="@string/settings_activity_label" android:theme="@style/settingsTheme" android:exported="true" android:directBootAware="true">
|
||||
<activity android:name="juloo.mornkeyboard2.SettingsActivity" android:icon="@mipmap/ic_launcher" android:theme="@style/settingsTheme" android:exported="true" android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="juloo.keyboard2.LauncherActivity" android:icon="@mipmap/ic_launcher" android:theme="@style/appTheme" android:exported="true" android:directBootAware="true">
|
||||
<activity android:name="juloo.mornkeyboard2.LauncherActivity" android:icon="@mipmap/ic_launcher" android:theme="@style/appTheme" android:exported="true" android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="juloo.mornkeyboard2.dict.DictionariesActivity" android:icon="@mipmap/ic_launcher" android:theme="@style/appTheme" android:exported="true" android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
<!-- To query enabled input methods for voice IME detection -->
|
||||
|
||||
@@ -7,10 +7,16 @@ Thanks for contributing :)
|
||||
The application uses Gradle and can be used with Android Studio, but using
|
||||
Android Studio is not required. The build dependencies are:
|
||||
- OpenJDK 17
|
||||
- Android SDK: build tools (minimum `28.0.1`), platform `30`
|
||||
- Android SDK: build tools, platform `36`
|
||||
|
||||
Python 3 is required to update generated files but not to build the app.
|
||||
|
||||
Make sure the Git submodules are initialized and point to the right revision:
|
||||
|
||||
```sh
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
For Android Studio users, no more setup is needed.
|
||||
|
||||
For Nix users, the right environment can be obtained with `nix-shell ./shell.nix`.
|
||||
@@ -173,13 +179,13 @@ Remove the `<!--` and `-->` parts and change the text.
|
||||
|
||||
### Adding a translation
|
||||
|
||||
The preferred method for translating the app is to use Weblate:
|
||||
https://hosted.weblate.org/engage/unexpected-keyboard/
|
||||
|
||||
The `res/values-<language_tag>/strings.xml` file must be created by copying the
|
||||
default translation in `res/values/strings.xml`, which contain the structure of
|
||||
the file and the English strings.
|
||||
|
||||
To check that `strings.xml` is formatted correctly, run
|
||||
`python sync_translations.py`. This will modify your files.
|
||||
|
||||
Store descriptions in `fastlane/metadata/android/` are updated automatically.
|
||||
Translating changelogs is not useful.
|
||||
|
||||
@@ -190,18 +196,46 @@ As translations need to be updated regularly, you can subscribe to this issue
|
||||
to receive a notification when an update is needed:
|
||||
https://github.com/Julow/Unexpected-Keyboard/issues/373
|
||||
|
||||
### Adding Compose key sequences
|
||||
### Adding symbols to Shift, Fn, Compose and other modifiers
|
||||
|
||||
New Compose sequences can be added into `srcs/compose/compose/extra.json`.
|
||||
If a entirely new family of sequences were to be added, a new `.json` file can
|
||||
be created in the same directory to host them.
|
||||
New key combinations can be added to builtin modifiers in the following files:
|
||||
|
||||
### Adding key combinations
|
||||
- Shift in `srcs/compose/shift.json`.
|
||||
- Fn in `srcs/compose/fn.json`.
|
||||
- Compose in `srcs/compose/compose/extra.json`.
|
||||
- Other modifiers are defined in the `accent_*.json` files in `srcs/compose`.
|
||||
|
||||
Key combinations are defined in `srcs/juloo.keyboard2/KeyModifier.java`.
|
||||
For example, keys modified by the `Fn` key are defined in method
|
||||
`apply_fn_char`.
|
||||
Generated code must then be updated by running:
|
||||
|
||||
Keys with special meaning are defined in `KeyValue.java` in method
|
||||
`getKeyByName`. Their special action are defined in `KeyEventHandler.java` in
|
||||
method `key_up`
|
||||
```
|
||||
./gradlew compileComposeSequences
|
||||
```
|
||||
|
||||
These files describe each symbols that get transformed when a given modifier is
|
||||
activated, in JSON format. For example:
|
||||
|
||||
Example from `fn.json`, when `Fn` is activated, `<` becomes `«`:
|
||||
```json
|
||||
{
|
||||
"<": "«",
|
||||
}
|
||||
```
|
||||
|
||||
The result of a sequence can be a key name. See the list of key names in
|
||||
[doc/Possible-key-values.md](doc/Possible-key-values.md). For example from
|
||||
`fn.json`, when `Fn` is activated, space becomes `nbsp`:
|
||||
```json
|
||||
{
|
||||
" ": "nbsp",
|
||||
}
|
||||
```
|
||||
|
||||
Compose sequences are made of several steps. For example, the sequence
|
||||
`Compose V s = Š` is defined as:
|
||||
```json
|
||||
{
|
||||
"V": {
|
||||
"s": "Š"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
# Unexpected Keyboard
|
||||
# Unexpected Keyboard [<img src="https://hosted.weblate.org/widget/unexpected-keyboard/svg-badge.svg" alt="État de la traduction" />](https://hosted.weblate.org/engage/unexpected-keyboard/)
|
||||
|
||||
---
|
||||
|
||||
⚠️ Google wants to kill the open-source Android community.
|
||||
|
||||
See [keepandroidopen.org](https://keepandroidopen.org/) and the [F-Droid blog](https://f-droid.org/en/2025/09/29/google-developer-registration-decree.html).
|
||||
|
||||
---
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
@@ -16,7 +24,7 @@ The main feature is that you can type more characters by swiping the keys toward
|
||||
This application was originally designed for programmers using Termux.
|
||||
Now perfect for everyday use.
|
||||
|
||||
This application contains no ads, doesn't make any network requests and is Open Source.
|
||||
This application contains no ads and is open source.
|
||||
|
||||
Usage: to apply the symbols located in the corners of each key, slide your finger in the direction of the symbols. For example, the Settings are opened by sliding in the left down corner.
|
||||
|
||||
@@ -24,10 +32,17 @@ Usage: to apply the symbols located in the corners of each key, slide your finge
|
||||
| --- | --- | --- |
|
||||
| <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" alt="Screenshot-4" /> | <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" alt="Screenshot-5" /> | <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png" alt="Screenshot-6" /> |
|
||||
|
||||
## Similar apps
|
||||
* [Calculator++](https://git.bubu1.eu/Bubu/android-calculatorpp) - Calculator with a similar UX, swipe to corners for advanced math symbols and operators.
|
||||
## Help translate the application
|
||||
|
||||
Improve the application translations [using Weblate](https://hosted.weblate.org/engage/unexpected-keyboard/).
|
||||
|
||||
[<img src="https://hosted.weblate.org/widget/unexpected-keyboard/multi-auto.svg" alt="État de la traduction" />](https://hosted.weblate.org/engage/unexpected-keyboard/)
|
||||
|
||||
## Contributing
|
||||
|
||||
For instructions on building the application, see
|
||||
[Contributing](CONTRIBUTING.md).
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
The [NLnet foundation](https://nlnet.nl/) funded the work on the spell checking feature.
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.1.1'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'juloo.keyboard2'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "juloo.keyboard2"
|
||||
minSdk 21
|
||||
targetSdkVersion 35
|
||||
versionCode 45
|
||||
versionName "1.30.2"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['srcs/juloo.keyboard2']
|
||||
res.srcDirs = ['res', 'build/generated-resources']
|
||||
assets.srcDirs = ['assets']
|
||||
}
|
||||
|
||||
test {
|
||||
java.srcDirs = ['test']
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// can be uploaded to GitHub secrets by following instructions in CONTRIBUTING.md
|
||||
// in order to always receive correctly signed debug APKs from the CI.
|
||||
debug {
|
||||
storeFile(System.env.DEBUG_KEYSTORE ? file(System.env.DEBUG_KEYSTORE) : file("debug.keystore"))
|
||||
storePassword(System.env.DEBUG_KEYSTORE_PASSWORD ? "$System.env.DEBUG_KEYSTORE_PASSWORD" : "debug0")
|
||||
keyAlias(System.env.DEBUG_KEY_ALIAS ? "$System.env.DEBUG_KEY_ALIAS" : "debug")
|
||||
keyPassword(System.env.DEBUG_KEY_PASSWORD ? "$System.env.DEBUG_KEY_PASSWORD" : "debug0")
|
||||
}
|
||||
|
||||
release {
|
||||
if (System.env.RELEASE_KEYSTORE) {
|
||||
storeFile file(System.env.RELEASE_KEYSTORE)
|
||||
storePassword "$System.env.RELEASE_KEYSTORE_PASSWORD"
|
||||
keyAlias "$System.env.RELEASE_KEY_ALIAS"
|
||||
keyPassword "$System.env.RELEASE_KEY_PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
debuggable false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
||||
resValue "string", "app_name", "@string/app_name_release"
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
|
||||
debug {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
resValue "string", "app_name", "@string/app_name_debug"
|
||||
resValue "bool", "debug_logs", "true"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
// Name outputs after the application ID.
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
outputFileName = "${applicationId}.apk"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// Translation are already checked by 'syncTranslations'
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('buildKeyboardFont') {
|
||||
println "\nBuilding assets/special_font.ttf"
|
||||
mkdir "$buildDir"
|
||||
exec {
|
||||
workingDir "$projectDir/srcs/special_font"
|
||||
def svgFiles = workingDir.listFiles().findAll {
|
||||
it.isFile() && it.name.endsWith(".svg")
|
||||
}
|
||||
commandLine("fontforge", "-lang=ff", "-script", "build.pe", "$buildDir/special_font.ttf", *svgFiles)
|
||||
}
|
||||
copy {
|
||||
from "$buildDir/special_font.ttf"
|
||||
into "assets"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('genEmojis') {
|
||||
println "\nGenerating res/raw/emojis.txt"
|
||||
exec {
|
||||
workingDir = projectDir
|
||||
commandLine "python", "gen_emoji.py"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test).configureEach {
|
||||
dependsOn 'genLayoutsList'
|
||||
dependsOn 'checkKeyboardLayouts'
|
||||
dependsOn 'syncTranslations'
|
||||
dependsOn 'compileComposeSequences'
|
||||
}
|
||||
|
||||
tasks.register('genLayoutsList') {
|
||||
println "\nGenerating res/values/layouts.xml"
|
||||
exec {
|
||||
workingDir = projectDir
|
||||
commandLine "python", "gen_layouts.py"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('checkKeyboardLayouts') {
|
||||
println "\nChecking layouts"
|
||||
exec {
|
||||
def layouts = new File(projectDir, "srcs/layouts").listFiles().findAll {
|
||||
it.name.endsWith(".xml")
|
||||
}
|
||||
workingDir = projectDir
|
||||
commandLine("python", "check_layout.py", *layouts)
|
||||
standardOutput = new FileOutputStream("${projectDir}/check_layout.output")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('syncTranslations') {
|
||||
println "\nUpdating translations"
|
||||
exec {
|
||||
workingDir = projectDir
|
||||
commandLine "python", "sync_translations.py"
|
||||
}
|
||||
}
|
||||
|
||||
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(".py") && !it.name.endsWith(".md")
|
||||
}
|
||||
workingDir = projectDir
|
||||
commandLine("python", "srcs/compose/compile.py", *sequences)
|
||||
standardOutput = new FileOutputStream("${projectDir}/${out}")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn += "initDebugKeystore"
|
||||
dependsOn += "copyRawQwertyUS"
|
||||
dependsOn += "copyLayoutDefinitions"
|
||||
}
|
||||
|
||||
tasks.register('initDebugKeystore') {
|
||||
if (!file("debug.keystore").exists()) {
|
||||
println "Initializing default debug keystore"
|
||||
exec {
|
||||
// A shell script might be needed if this line requires input from the user
|
||||
commandLine "keytool", "-genkeypair", "-dname", "cn=d, ou=e, o=b, c=ug", "-alias", "debug", "-keypass", "debug0", "-keystore", "debug.keystore", "-keyalg", "rsa", "-storepass", "debug0", "-validity", "10000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// latn_qwerty_us is used as a raw resource by the custom layout option.
|
||||
tasks.register('copyRawQwertyUS')
|
||||
{
|
||||
copy {
|
||||
from "srcs/layouts/latn_qwerty_us.xml"
|
||||
into "build/generated-resources/raw"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('copyLayoutDefinitions')
|
||||
{
|
||||
copy {
|
||||
from "srcs/layouts"
|
||||
include "*.xml"
|
||||
into "build/generated-resources/xml"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import java.io.FileOutputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application") version "8.13.2"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Following versions of androidx.window require sdk version 23
|
||||
implementation("androidx.window:window-java:1.4.0")
|
||||
implementation("androidx.core:core:1.16.0") // Version 1.17.0 available with sdk 36
|
||||
implementation(project(":libs:libswipetype:swipetype-android"))
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "juloo.mornkeyboard2"
|
||||
compileSdkVersion = "android-36"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "juloo.mornkeyboard2"
|
||||
minSdk = 21
|
||||
targetSdk { version = release(36) }
|
||||
versionCode = 55
|
||||
versionName = "2.0.4"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
named("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.srcDirs("srcs/juloo.mornkeyboard2", "vendor/cdict/java/juloo.cdict")
|
||||
res.srcDirs("res", "build/generated-resources")
|
||||
assets.srcDirs("assets")
|
||||
}
|
||||
|
||||
named("test") {
|
||||
java.srcDirs("test")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// can be uploaded to GitHub secrets by following instructions in CONTRIBUTING.md
|
||||
// in order to always receive correctly signed debug APKs from the CI.
|
||||
named("debug") {
|
||||
storeFile = file(System.getenv("DEBUG_KEYSTORE") ?: "debug.keystore")
|
||||
storePassword = System.getenv("DEBUG_KEYSTORE_PASSWORD") ?: "debug0"
|
||||
keyAlias = System.getenv("DEBUG_KEY_ALIAS") ?: "debug"
|
||||
keyPassword = System.getenv("DEBUG_KEY_PASSWORD") ?: "debug0"
|
||||
}
|
||||
|
||||
create("release") {
|
||||
val ks = System.getenv("RELEASE_KEYSTORE")
|
||||
if (ks != null) {
|
||||
storeFile = file(ks)
|
||||
storePassword = System.getenv("RELEASE_KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("RELEASE_KEY_ALIAS")
|
||||
keyPassword = System.getenv("RELEASE_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro")
|
||||
isShrinkResources = true
|
||||
isDebuggable = false
|
||||
resValue("string", "app_name", "@string/app_name_release")
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
|
||||
named("debug") {
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
isDebuggable = true
|
||||
applicationIdSuffix = ".advanced.debug"
|
||||
resValue("string", "app_name", "@string/app_name_debug")
|
||||
resValue("bool", "debug_logs", "true")
|
||||
signingConfig = signingConfigs["debug"]
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This raises an error with an informative message instead of the confusing
|
||||
// ndk-build errors that occur when submodules are not initialized.
|
||||
gradle.projectsEvaluated {
|
||||
if (!file("vendor/cdict/java").exists())
|
||||
throw GradleException("Git submodules not initialized. Run 'git submodule update --init'")
|
||||
}
|
||||
|
||||
val buildKeyboardFont by tasks.registering(Exec::class) {
|
||||
val `in` = projectDir.resolve("srcs/special_font")
|
||||
val out = layout.projectDirectory.file("assets/special_font.ttf")
|
||||
inputs.dir(`in`)
|
||||
outputs.file(out)
|
||||
doFirst { println("\nBuilding assets/special_font.ttf") }
|
||||
workingDir = `in`
|
||||
val svgFiles = `in`.listFiles()!!.filter {
|
||||
it.isFile && it.name.endsWith(".svg")
|
||||
}.toTypedArray()
|
||||
commandLine("fontforge", "-lang=ff", "-script", "build.pe", out.asFile.absolutePath, *svgFiles)
|
||||
}
|
||||
|
||||
val genEmojis by tasks.registering(Exec::class) {
|
||||
doFirst { println("\nGenerating res/raw/emojis.txt") }
|
||||
workingDir = projectDir
|
||||
commandLine("python", "gen_emoji.py")
|
||||
}
|
||||
|
||||
val genLayoutsList by tasks.registering(Exec::class) {
|
||||
inputs.dir(projectDir.resolve("srcs/layouts"))
|
||||
outputs.file(projectDir.resolve("res/values/layouts.xml"))
|
||||
doFirst { println("\nGenerating res/values/layouts.xml") }
|
||||
workingDir = projectDir
|
||||
commandLine("python", "gen_layouts.py")
|
||||
}
|
||||
|
||||
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) }
|
||||
workingDir = projectDir
|
||||
commandLine("python", "gen_method_xml.py")
|
||||
}
|
||||
|
||||
val checkKeyboardLayouts by tasks.registering(Exec::class) {
|
||||
inputs.dir(projectDir.resolve("srcs/layouts"))
|
||||
inputs.file(projectDir.resolve("srcs/juloo.mornkeyboard2/KeyValue.java"))
|
||||
outputs.file(projectDir.resolve("check_layout.output"))
|
||||
doFirst { println("\nChecking layouts") }
|
||||
workingDir = projectDir
|
||||
commandLine("python", "check_layout.py")
|
||||
}
|
||||
|
||||
val compileComposeSequences by tasks.registering(Exec::class) {
|
||||
val `in` = projectDir.resolve("srcs/compose")
|
||||
val out = projectDir.resolve("srcs/juloo.mornkeyboard2/ComposeKeyData.java")
|
||||
inputs.dir(`in`)
|
||||
outputs.file(out)
|
||||
doFirst { println("\nGenerating $out") }
|
||||
val sequences = `in`.listFiles { it: File ->
|
||||
!it.name.endsWith(".py") && !it.name.endsWith(".md")
|
||||
}!!.map { it.absolutePath }.toTypedArray()
|
||||
workingDir = projectDir
|
||||
commandLine("python", `in`.resolve("compile.py").absolutePath, *sequences)
|
||||
doFirst { standardOutput = FileOutputStream(out) }
|
||||
}
|
||||
|
||||
tasks.withType(Test::class).configureEach {
|
||||
dependsOn(genLayoutsList, checkKeyboardLayouts, compileComposeSequences, genMethodXml)
|
||||
}
|
||||
|
||||
val initDebugKeystore by tasks.registering(Exec::class) {
|
||||
doFirst { println("Initializing default debug keystore") }
|
||||
isEnabled = !file("debug.keystore").exists()
|
||||
// A shell script might be needed if this line requires input from the user
|
||||
commandLine("keytool", "-genkeypair", "-dname", "cn=d, ou=e, o=b, c=ug", "-alias", "debug", "-keypass", "debug0", "-keystore", "debug.keystore", "-keyalg", "rsa", "-storepass", "debug0", "-validity", "10000")
|
||||
}
|
||||
|
||||
// latn_qwerty_us is used as a raw resource by the custom layout option.
|
||||
val copyRawQwertyUS by tasks.registering(Copy::class) {
|
||||
from("srcs/layouts/latn_qwerty_us.xml")
|
||||
into("build/generated-resources/raw")
|
||||
}
|
||||
|
||||
val copyLayoutDefinitions by tasks.registering(Copy::class) {
|
||||
from("srcs/layouts")
|
||||
include("*.xml")
|
||||
into("build/generated-resources/xml")
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(initDebugKeystore, copyRawQwertyUS, copyLayoutDefinitions)
|
||||
// 'mustRunAfter' defines ordering between tasks (which is required by
|
||||
// Gradle) but doesn't create a dependency. These rules update files that are
|
||||
// checked in the repository that don't need to be updated during regular
|
||||
// builds.
|
||||
mustRunAfter(genEmojis, genLayoutsList, compileComposeSequences, genMethodXml)
|
||||
}
|
||||
@@ -1,185 +1,32 @@
|
||||
# arab_alt
|
||||
Layout includes some ASCII punctuation but not all, missing: !, ", ', +, -, /, :, ;, <, =, >, ?, [, \, ], _, |, ~
|
||||
1 warnings
|
||||
# arab_hamvaj_tly
|
||||
Layout includes some ASCII punctuation but not all, missing: ", %, ', ,, /, ;, <, =, >, ?, [, \, ], _, `, {, |, }
|
||||
Layout doesn't define some important keys, missing: loc esc
|
||||
2 warnings
|
||||
# arab_pc
|
||||
Layout includes some ASCII punctuation but not all, missing: !, ', +, ;, ?, \, |
|
||||
1 warnings
|
||||
# arab_pc_ckb
|
||||
Layout includes some ASCII punctuation but not all, missing: ", %, ', +, ,, ;, <, =, >, ?, `, |, ~
|
||||
1 warnings
|
||||
# arab_pc_ckb_fa
|
||||
Layout includes some ASCII punctuation but not all, missing: ", #, $, %, &, ', ,, /, ;, ?, @, \, ^, `, |, ~
|
||||
Layout doesn't define some important keys, missing: loc esc, loc tab
|
||||
2 warnings
|
||||
# arab_pc_hindu
|
||||
Layout includes some ASCII punctuation but not all, missing: !, ', +, ;, ?, \, |
|
||||
1 warnings
|
||||
# arab_pc_ir
|
||||
Layout includes some ASCII punctuation but not all, missing: ", %, ', ,, /, ;, <, =, >, ?, [, \, ], `, {, |, }
|
||||
1 warnings
|
||||
# armenian_ph_am
|
||||
0 warnings
|
||||
# beng_national
|
||||
Layout includes some ASCII punctuation but not all, missing: $
|
||||
1 warnings
|
||||
# beng_provat
|
||||
Layout includes some ASCII punctuation but not all, missing: $, &, *, ., /, <, >, [, \, ], `, {, |, }
|
||||
1 warnings
|
||||
# cyrl_jcuken_kk
|
||||
0 warnings
|
||||
# cyrl_jcuken_ru
|
||||
0 warnings
|
||||
# cyrl_jcuken_uk
|
||||
0 warnings
|
||||
# cyrl_lynyertz_sr
|
||||
0 warnings
|
||||
# cyrl_ueishsht
|
||||
0 warnings
|
||||
# cyrl_yaverti
|
||||
Layout includes some ASCII punctuation but not all, missing: ~
|
||||
1 warnings
|
||||
# cyrl_yqukeng_tj
|
||||
Layout doesn't define some important keys, missing: loc esc, loc tab
|
||||
These keys are now added automatically, unexpected: f11_placeholder, f12_placeholder
|
||||
2 warnings
|
||||
# cyrl_yxukeng_os
|
||||
Layout includes some ASCII punctuation but not all, missing: ", #, $, &, ', @, [, ], ~
|
||||
Layout doesn't define some important keys, missing: loc esc, loc tab
|
||||
These keys are now added automatically, unexpected: f11_placeholder, f12_placeholder
|
||||
3 warnings
|
||||
# deva_alt
|
||||
Layout includes some ASCII punctuation but not all, missing: #, $, %, &, ', (, ), +, ., /, :, <, =, >, [, \, ], ^, _, `, {, |, }, ~
|
||||
Layout doesn't define some important keys, missing: loc esc, loc tab
|
||||
2 warnings
|
||||
# deva_inscript
|
||||
Duplicate keys: ।
|
||||
Layout includes some ASCII punctuation but not all, missing: ", $, ', ^, _, `, |
|
||||
2 warnings
|
||||
# deva_phonetic_in
|
||||
Duplicate keys: ट
|
||||
1 warnings
|
||||
# georgian_mes
|
||||
0 warnings
|
||||
# georgian_qwerty
|
||||
0 warnings
|
||||
# grek_qwerty
|
||||
Duplicate keys: ;
|
||||
1 warnings
|
||||
# guj_phonetic_in
|
||||
Duplicate keys: ટ, ડ
|
||||
1 warnings
|
||||
# hang_dubeolsik_kr
|
||||
Layout doesn't define some important keys, missing: loc esc, loc tab
|
||||
1 warnings
|
||||
# hebr_1_il
|
||||
Layout includes some ASCII punctuation but not all, missing: (, ), <, >, [, ], {, }
|
||||
1 warnings
|
||||
# hebr_2_il
|
||||
Layout includes some ASCII punctuation but not all, missing: (, ), <, >, [, ], {, }
|
||||
1 warnings
|
||||
# kann_kannada
|
||||
Layout includes some ASCII punctuation but not all, missing: #, $, %, (, ), *, +, /, <, =, >, [, \, ], ^, `, {, |, }, ~
|
||||
Layout doesn't define some important keys, missing: loc esc, loc tab
|
||||
2 warnings
|
||||
# latn_azerty_be
|
||||
0 warnings
|
||||
# latn_azerty_fr
|
||||
0 warnings
|
||||
# latn_bepo_fr
|
||||
0 warnings
|
||||
# latn_bone
|
||||
Layout doesn't define some important keys, missing: loc esc, loc tab
|
||||
Layout redefines the bottom row but some important keys are missing, missing: cursor_left, cursor_right, loc compose, loc end, loc home, loc page_down, loc page_up, loc switch_clipboard, loc switch_greekmath, loc voice_typing, switch_backward
|
||||
2 warnings
|
||||
# latn_colemak
|
||||
Some keys contain whitespaces, unexpected: ́
|
||||
1 warnings
|
||||
# latn_dvorak
|
||||
0 warnings
|
||||
# latn_neo2
|
||||
Layout redefines the bottom row but some important keys are missing, missing: loc switch_clipboard
|
||||
1 warnings
|
||||
# latn_qwerty_br
|
||||
0 warnings
|
||||
# latn_qwerty_cy
|
||||
0 warnings
|
||||
# latn_qwerty_cz
|
||||
0 warnings
|
||||
# latn_qwerty_da
|
||||
0 warnings
|
||||
# latn_qwerty_es
|
||||
0 warnings
|
||||
# latn_qwerty_et
|
||||
0 warnings
|
||||
# latn_qwerty_ga
|
||||
0 warnings
|
||||
# latn_qwerty_gb
|
||||
0 warnings
|
||||
# latn_qwerty_haw
|
||||
0 warnings
|
||||
# latn_qwerty_hu
|
||||
0 warnings
|
||||
# latn_qwerty_jp
|
||||
0 warnings
|
||||
# latn_qwerty_kk
|
||||
0 warnings
|
||||
# latn_qwerty_lv
|
||||
0 warnings
|
||||
# latn_qwerty_no
|
||||
0 warnings
|
||||
# latn_qwerty_pl
|
||||
0 warnings
|
||||
# latn_qwerty_ro
|
||||
0 warnings
|
||||
# latn_qwerty_se
|
||||
Duplicate keys: !, ', ,, -, ., ?
|
||||
1 warnings
|
||||
# latn_qwerty_sk
|
||||
0 warnings
|
||||
# latn_qwerty_sr
|
||||
0 warnings
|
||||
# latn_qwerty_tly
|
||||
Duplicate keys: a, c, j, q
|
||||
Layout doesn't define some important keys, missing: loc esc, loc tab
|
||||
2 warnings
|
||||
# latn_qwerty_tr
|
||||
0 warnings
|
||||
# latn_qwerty_us
|
||||
0 warnings
|
||||
# latn_qwerty_vi
|
||||
0 warnings
|
||||
# latn_qwertz
|
||||
0 warnings
|
||||
# latn_qwertz_cz
|
||||
0 warnings
|
||||
# latn_qwertz_cz_diacritics
|
||||
0 warnings
|
||||
# latn_qwertz_cz_multifunctional
|
||||
Layout includes some ASCII punctuation but not all, missing: `
|
||||
1 warnings
|
||||
# latn_qwertz_de
|
||||
0 warnings
|
||||
# latn_qwertz_fr_ch
|
||||
0 warnings
|
||||
# latn_qwertz_hu
|
||||
0 warnings
|
||||
# latn_qwertz_sk
|
||||
Layout includes some ASCII punctuation but not all, missing: `
|
||||
1 warnings
|
||||
# latn_qwertz_sq
|
||||
0 warnings
|
||||
# latn_workman_us
|
||||
0 warnings
|
||||
# shaw_imperial_en
|
||||
0 warnings
|
||||
# tamil_default
|
||||
0 warnings
|
||||
# urdu_phonetic_ur
|
||||
Duplicate keys:
|
||||
Layout includes some ASCII punctuation but not all, missing: <, >, ?, `, |, ~
|
||||
Some keys contain whitespaces, unexpected:
|
||||
3 warnings
|
||||
arab_alt: Layout includes some ASCII punctuation but not all, missing: !, ", ', +, -, /, :, ;, <, =, >, ?, [, \, ], _, |, ~
|
||||
arab_hamvaj_tly: Layout includes some ASCII punctuation but not all, missing: ", %, ', ,, /, ;, <, =, >, ?, [, \, ], _, `, {, |, }
|
||||
arab_pc: Layout includes some ASCII punctuation but not all, missing: !, ', +, ;, ?, \, |
|
||||
arab_pc_ckb: Layout includes some ASCII punctuation but not all, missing: ", %, ', +, ,, ;, <, =, >, ?, `, |, ~
|
||||
arab_pc_ckb_fa: Layout includes some ASCII punctuation but not all, missing: ", #, $, %, &, ', ,, /, ;, ?, @, \, ^, `, |, ~
|
||||
arab_pc_hindu: Layout includes some ASCII punctuation but not all, missing: !, ', +, ;, ?, \, |
|
||||
arab_pc_ir: Layout includes some ASCII punctuation but not all, missing: ", %, ', ,, /, ;, <, =, >, ?, [, \, ], `, {, |, }
|
||||
armn_cpbsmo: Layout includes some ASCII punctuation but not all, missing: !, ", #, $, %, &, ', *, +, <, =, >, ?, @, [, ], ^, `, {, }, ~
|
||||
armn_kvertc: Layout includes some ASCII punctuation but not all, missing: !, ", #, $, %, &, ', *, +, <, =, >, ?, @, [, ], ^, `, {, }, ~
|
||||
beng_assamese: Layout includes some ASCII punctuation but not all, missing: #, $, %, &, ', *, +, -, /, :, ;, <, =, >, ?, [, \, ], ^, _, `, {, |, }, ~
|
||||
beng_national: Layout includes some ASCII punctuation but not all, missing: $
|
||||
beng_provat: Layout includes some ASCII punctuation but not all, missing: $, &, *, ., /, <, >, [, \, ], `, {, |, }
|
||||
cyrl_jiuken: Layout includes some ASCII punctuation but not all, missing: #, $, &, ', *, +, <, =, >, @, [, ], ^, _, `, {, }, ~
|
||||
cyrl_lynyertdz_mk: Layout includes some ASCII punctuation but not all, missing: "
|
||||
cyrl_yaverti: Layout includes some ASCII punctuation but not all, missing: ~
|
||||
cyrl_yawerty: Layout includes some ASCII punctuation but not all, missing: #, $, &, ', *, <, >, @, [, ], ^, `, {, }, ~
|
||||
cyrl_yxukeng_os: Layout includes some ASCII punctuation but not all, missing: ", #, $, &, ', @, [, ], ~
|
||||
deva_alt: Layout includes some ASCII punctuation but not all, missing: #, $, %, &, ', (, ), *, +, ., /, :, <, =, >, [, \, ], ^, _, `, {, |, }, ~
|
||||
deva_inscript: Layout includes some ASCII punctuation but not all, missing: ", $, ', ^, _, `, |
|
||||
hebr_1_il: Layout includes some ASCII punctuation but not all, missing: (, ), <, >, [, ], {, }
|
||||
hebr_2_il: Layout includes some ASCII punctuation but not all, missing: (, ), <, >, [, ], {, }
|
||||
kann_kannada: Layout includes some ASCII punctuation but not all, missing: #, $, %, (, ), *, +, /, <, =, >, [, \, ], ^, `, {, |, }, ~
|
||||
latin_kbdtuf_tr: Layout includes some ASCII punctuation but not all, missing: $
|
||||
latin_kbdtuf_tr: These keys are now added automatically, unexpected: f11_placeholder, f12_placeholder
|
||||
latin_kbdtuf_tr: Missing programming keys, missing: loc esc, loc tab
|
||||
latn_colemak: Some keys contain whitespaces, unexpected: ́
|
||||
latn_dvorak: Missing important key, missing: loc capslock
|
||||
latn_neo2: Layout redefines the bottom row but some important keys are missing, missing: loc switch_clipboard
|
||||
latn_qwertz_cz_multifunctional: Layout includes some ASCII punctuation but not all, missing: `
|
||||
latn_qwertz_sk: Layout includes some ASCII punctuation but not all, missing: `
|
||||
latn_qzerty_it: Layout includes some ASCII punctuation but not all, missing: #, $, \, `, ~
|
||||
urdu_phonetic_ur: Layout includes some ASCII punctuation but not all, missing: <, >, ?, `, |, ~
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
import sys, os
|
||||
import sys, os, glob, re
|
||||
|
||||
warning_count = 0
|
||||
layout_file_name = 0
|
||||
warnings = []
|
||||
|
||||
KNOWN_NOT_LAYOUT = set([
|
||||
"number_row", "numpad", "pin",
|
||||
@@ -14,10 +15,12 @@ KEY_ATTRIBUTES = set([
|
||||
"c", "nw", "ne", "sw", "se", "w", "e", "n", "s"
|
||||
])
|
||||
|
||||
# Keys defined in KeyValue.java
|
||||
known_keys = set()
|
||||
|
||||
def warn(msg):
|
||||
global warning_count
|
||||
print(msg)
|
||||
warning_count += 1
|
||||
global warnings
|
||||
warnings.append("%s: %s" % (layout_file_name, msg))
|
||||
|
||||
def key_list_str(keys):
|
||||
return ", ".join(sorted(list(keys)))
|
||||
@@ -40,42 +43,53 @@ def unexpected_keys(keys, symbols, msg):
|
||||
if len(unexpected) > 0:
|
||||
warn("%s, unexpected: %s" % (msg, key_list_str(unexpected)))
|
||||
|
||||
# Write to [keys] and [dup].
|
||||
def parse_row_from_et(row, keys, dup):
|
||||
for key in row:
|
||||
for attr in key.keys():
|
||||
def duplicates(keys):
|
||||
dup = [ k for k, key_elts in keys.items() if len(key_elts) >= 2 ]
|
||||
if len(dup) > 0:
|
||||
warn("Duplicate keys: " + key_list_str(dup))
|
||||
|
||||
def should_have_role(keys_map, role, keys):
|
||||
def is_center_key(key):
|
||||
def center(key_elt): return key_elt.get("key0", key_elt.get("c"))
|
||||
return any(( center(key_elt) == key for key_elt in keys_map.get(key, []) ))
|
||||
def key_roles(key):
|
||||
return ( key_elt.get("role", "normal") for key_elt in keys_map[key] )
|
||||
for key in keys:
|
||||
if is_center_key(key) and role not in key_roles(key):
|
||||
warn("Key '%s' is not on a key with role=\"%s\"" % (key, role))
|
||||
|
||||
# Write to [keys], dict of keyvalue to the key elements they appear in
|
||||
def parse_row_from_et(row, keys):
|
||||
for key_elt in row:
|
||||
for attr in key_elt.keys():
|
||||
if attr in KEY_ATTRIBUTES:
|
||||
k = key.get(attr).removeprefix("\\")
|
||||
if k in keys: dup.add(k)
|
||||
keys.add(k)
|
||||
k = key_elt.get(attr).removeprefix("\\")
|
||||
keys.setdefault(k, []).append(key_elt)
|
||||
|
||||
def parse_layout(fname):
|
||||
keys = set()
|
||||
dup = set()
|
||||
keys = {}
|
||||
root = ET.parse(fname).getroot()
|
||||
if root.tag != "keyboard":
|
||||
return None
|
||||
for row in root:
|
||||
parse_row_from_et(row, keys, dup)
|
||||
return root, keys, dup
|
||||
parse_row_from_et(row, keys)
|
||||
return root, keys
|
||||
|
||||
def parse_row(fname):
|
||||
keys = set()
|
||||
dup = set()
|
||||
keys = {}
|
||||
root = ET.parse(fname).getroot()
|
||||
if root.tag != "row":
|
||||
return None
|
||||
parse_row_from_et(root, keys, dup)
|
||||
return root, keys, dup
|
||||
parse_row_from_et(root, keys)
|
||||
return root, keys
|
||||
|
||||
def check_layout(layout):
|
||||
root, keys, dup = layout
|
||||
if len(dup) > 0: warn("Duplicate keys: " + key_list_str(dup))
|
||||
root, keys_map = layout
|
||||
keys = set(keys_map.keys())
|
||||
duplicates(keys_map)
|
||||
missing_some_of(keys, "~!@#$%^&*(){}`[]=\\-_;:/.,?<>'\"+|", "ASCII punctuation")
|
||||
missing_some_of(keys, "0123456789", "digits")
|
||||
missing_required(keys,
|
||||
["loc esc", "loc tab", "backspace", "delete"],
|
||||
"Layout doesn't define some important keys")
|
||||
missing_required(keys, ["backspace", "delete"], "Layout doesn't define some important keys")
|
||||
unexpected_keys(keys,
|
||||
["copy", "paste", "cut", "selectAll", "shareText",
|
||||
"pasteAsPlainText", "undo", "redo" ],
|
||||
@@ -89,8 +103,12 @@ def check_layout(layout):
|
||||
unexpected_keys(keys, filter(lambda k: k.strip()!=k, keys), "Some keys contain whitespaces")
|
||||
unexpected_keys(keys, ["f11_placeholder", "f12_placeholder"], "These keys are now added automatically")
|
||||
|
||||
_, bottom_row_keys, _ = parse_row("res/xml/bottom_row.xml")
|
||||
if root.get("script", "latin") == "latin":
|
||||
missing_required(keys, ["shift", "loc capslock"], "Missing important key")
|
||||
missing_required(keys, ["loc esc", "loc tab"], "Missing programming keys")
|
||||
|
||||
_, bottom_row_keys_map = parse_row("res/xml/bottom_row.xml")
|
||||
bottom_row_keys = set(bottom_row_keys_map.keys())
|
||||
if root.get("bottom_row") == "false":
|
||||
missing_required(keys, bottom_row_keys,
|
||||
"Layout redefines the bottom row but some important keys are missing")
|
||||
@@ -101,15 +119,38 @@ def check_layout(layout):
|
||||
if root.get("script") == None:
|
||||
warn("Layout doesn't specify a script.")
|
||||
|
||||
for fname in sorted(sys.argv[1:]):
|
||||
should_have_role(keys_map, "action",
|
||||
[ "shift", "ctrl", "fn", "backspace", "enter" ])
|
||||
should_have_role(keys_map, "space_bar", [ "space" ])
|
||||
|
||||
keys_without_loc = set(( k.removeprefix("loc ") for k in keys ))
|
||||
# Keys with a len under 3 are often composed characters
|
||||
special_keys = set(( k for k in keys_without_loc if len(k) > 3 and ":" not in k ))
|
||||
unknown = special_keys.difference(known_keys)
|
||||
if len(unknown) > 0:
|
||||
warn("Layout contains unknown keys: %s" % key_list_str(unknown))
|
||||
|
||||
# Fill 'known_keys', which is used for some checks
|
||||
def parse_known_keys():
|
||||
global known_keys
|
||||
with open("srcs/juloo.mornkeyboard2/KeyValue.java", "r", encoding="utf-8") as f:
|
||||
known_keys = set(
|
||||
( m.group(1) for m in re.finditer('case "([^"]+)":', f.read()) )
|
||||
)
|
||||
|
||||
parse_known_keys()
|
||||
|
||||
for fname in sorted(glob.glob("srcs/layouts/*.xml")):
|
||||
layout_id, _ = os.path.splitext(os.path.basename(fname))
|
||||
if layout_id in KNOWN_NOT_LAYOUT:
|
||||
continue
|
||||
layout_file_name = layout_id
|
||||
layout = parse_layout(fname)
|
||||
if layout == None:
|
||||
print("Not a layout file: %s" % layout_id)
|
||||
warn("Not a layout file")
|
||||
else:
|
||||
print("# %s" % layout_id)
|
||||
warning_count = 0
|
||||
check_layout(layout)
|
||||
print("%d warnings" % warning_count)
|
||||
|
||||
with open("check_layout.output", "w", encoding="utf-8") as out:
|
||||
for w in warnings:
|
||||
print(w, file=out)
|
||||
|
||||
@@ -6,7 +6,7 @@ We recommend you keep your work in a file outside Unexpected Keyboard (named som
|
||||
Put initial contents into your file in one of these ways:
|
||||
* Copypaste the code Unexpected displays for _Custom layout_.
|
||||
* Make a copy of one of the built-in layouts found in [`/srcs/layouts`](https://github.com/Julow/Unexpected-Keyboard/tree/master/srcs/layouts).
|
||||
* Use the [web-based editor](https://unexpected-keyboard-layout-editor.lixquid.com/). Interact with this web page to define keys and swipes and move keys to desired positions, and it will write the XML code for you. You can make the web page put the XML in a text file or copy it to the clipboard.
|
||||
* Use the [web-based editor](https://domportera.github.io/app-unexpected-keyboard-layout-editor/). Interact with this web page to define keys and swipes and move keys to desired positions, and it will write the XML code for you. You can make the web page put the XML in a text file or copy it to the clipboard.
|
||||
|
||||
When you have prepared suitable XML code in one of these ways, copy it to the clipboard and paste it into Unexpected Keyboard.
|
||||
|
||||
@@ -44,17 +44,23 @@ The `<keyboard>`...`</keyboard>` pair follows the declaration tag and encloses t
|
||||
* `numpad_script`: The script to use for the numpad. This is useful for scripts where a different, non-ASCII set of numerals is used, like Devanagari and Arabic. It defaults to the same as `script`.
|
||||
|
||||
* `bottom_row`: Whether or not to show the built-in bottom row. It accepts `true` or `false`, and defaults to `true`. If your custom layout defines the bottom row, then specify `bottom_row="false"` to disable the built-in bottom row.
|
||||
+ We recommend your layout use the built-in bottom row, because it is still evolving and your layout will incorporate innovations in future versions. However, to define your own, the current definition of the bottom row is in [bottom_row.xml](https://github.com/Julow/Unexpected-Keyboard/res/xml/bottom_row.xml). You can copypaste this XML into your custom layout as a starting point.
|
||||
+ Likewise, the current definition of the top (number) row is in [number_row.xml](https://github.com/Julow/Unexpected-Keyboard/res/xml/number_row.xml).
|
||||
+ We recommend your layout use the built-in bottom row, because it is still evolving and your layout will incorporate innovations in future versions. However, to define your own, the current definition of the bottom row is in [bottom_row.xml](https://github.com/Julow/Unexpected-Keyboard/blob/master/res/xml/bottom_row.xml). You can copypaste this XML into your custom layout as a starting point.
|
||||
+ Likewise, the current definition of the top (number) row is in [number_row.xml](https://github.com/Julow/Unexpected-Keyboard/blob/master/res/xml/number_row.xml).
|
||||
|
||||
* `embedded_number_row`: Whether the layout has an embedded number row, and thus the "Show number row" setting shouldn't add another one. It accepts `true` or `false`, and defaults to `false`.
|
||||
|
||||
* `locale_extra_keys`: Whether Unexpected should add language-dependent extra keys from [method.xml](../res/xml/method.xml) to this layout. It accepts `true` or `false`, and defaults to `true`. To disable these automatic additions, specify `locale_extra_keys="false"`.
|
||||
|
||||
## Row
|
||||
The `<row>`...`</row>` pair encloses one row on the keyboard. It has only one optional property:
|
||||
The `<row>`...`</row>` pair encloses one row on the keyboard. It has the following optional property:
|
||||
* `height`: The height of the row: a positive floating-point value.
|
||||
|
||||
* `scale`: A positive floating-point value. If present, scale the width of each key so that the total is equal to the specified value, in key width unit.
|
||||
|
||||
A row's default height is 1.0 (one quarter of the keyboard height specified on the Settings menu). The `height` property makes the row taller or shorter than this. For example, if you define a 5-row keyboard but one row has `height="0.7"`, then the keyboard's total height is 4.7 units. If the total is different from 4.0, the keyboard will be taller or shorter than that specified in Settings.
|
||||
|
||||
(A row of keys is drawn with a minimum height of 0.5 even if you specify a smaller value for `height`. There is no such minimum for a row without keys, such as a spacer row.)
|
||||
|
||||
## Key
|
||||
The `<key />` tag defines a key on the keyboard. Its position in the sequence of keys inside `<row>`...`</row>` indicates its position in the row from left to right. What the key does is defined by optional properties.
|
||||
|
||||
@@ -93,7 +99,6 @@ The following optional properties define the effects of swipes:
|
||||
|
||||
You can define a swipe only once with either compass-point or numeric notation. Unexpected Keyboard automatically puts a small legend in that direction from the center of the key.
|
||||
|
||||
* `slider`: If `slider="true"`, and the key also has `w` and `e` properties, then the key tracks horizontal finger motion precisely and sends the `w` and `e` keystrokes repeatedly. In built-in layouts, this makes the space bar send left and right characters as the user slides on the space bar.
|
||||
* `anticircle`: The key value to send when doing an anti-clockwise gesture on the key.
|
||||
|
||||
### Layout
|
||||
@@ -131,7 +136,7 @@ This means that when the Fn modifier is on, the key `before` is changed into `af
|
||||
```
|
||||
This means that when the Ctrl modifier is on, the key `before` is changed into `after`. The `<ctrl />` mapping is special in that the Ctrl modifier is applied to `after` after the mapping.
|
||||
|
||||
The clockwise circle and the round-trip gestures are affected by both `<shift />` and `<fn />` mappings. The Shift mappings are used first and if that did not modify the key, the Fn mappings are used instead.
|
||||
The clockwise circle and the round-trip gestures are affected by the `<fn />` mappings. Otherwise, they are defined by the Shift mappings (including the `<shift />` mappings), then, if that did not modify the key, the builtin Fn mappings are used instead.
|
||||
|
||||
### Examples
|
||||
① Turkish keyboards use the Latin alphabet, but when "i" is shifted, it should produce "İ". This is achieved with the following mapping:
|
||||
@@ -146,6 +151,9 @@ The clockwise circle and the round-trip gestures are affected by both `<shift />
|
||||
```
|
||||
This maps Ctrl+в to Ctrl+V—not to v.
|
||||
|
||||
### Default mappings
|
||||
Unexpected Keyboard's built-in mappings are not expressed as a modmap but by a series of .json files in `https://github.com/Julow/Unexpected-Keyboard/tree/master/srcs/compose`. For example, the mappings for `fn` are in [`fn.json`](https://github.com/Julow/Unexpected-Keyboard/blob/master/srcs/compose/fn.json). These built-in mappings are common to every language.
|
||||
|
||||
## Portrait vs. landscape
|
||||
Unexpected Keyboard remembers *separately* which layout has last been used in portrait and landscape orientation. So you may have one custom layout for portrait orientation, but another custom layout for landscape orientation, and Unexpected Keyboard will switch between them without your intervention.
|
||||
|
||||
|
||||
@@ -1,43 +1,73 @@
|
||||
# Key values
|
||||
|
||||
This is an exhaustive list of special values accepted for the `key0` through `key8` or `nw` through `se` attributes on a key.
|
||||
A key value defines what a key on the keyboard does when pressed or swiped.
|
||||
|
||||
Any string that does not exactly match these will be printed verbatim.
|
||||
A key can output multiple characters, but cannot combine multiple built-in key values.
|
||||
Key values appear in the following places:
|
||||
|
||||
## Escape codes
|
||||
Value | Escape code for
|
||||
- In custom layouts, they are the value of: the `c` attribute, the compass-point attributes `nw` ... `se`, and the old-style `key0` ... `key8` attributes.
|
||||
- Internally, they are used in the definition of the "Add keys to the keyboard" setting.
|
||||
|
||||
Key values can be any of the following:
|
||||
|
||||
- The name of a special key. A complete list of valid special keys follows.
|
||||
|
||||
- An arbitrary sequence of characters not containing `:`.
|
||||
This results in a key that writes the specified characters.
|
||||
|
||||
- The syntax `legend:key_def`.
|
||||
`legend` is the visible legend on the keyboard. It cannot contain `:`.
|
||||
`key_def` can be:
|
||||
+ The name of a special key, as listed below.
|
||||
+ `'string'` An arbitrary string that can contain `:`. `'` can be added to the string as `` \' ``.
|
||||
+ `keyevent:keycode` An Android keycode. They are listed as `KEYCODE_...` in [KeyEvent](https://developer.android.com/reference/android/view/KeyEvent#summary).
|
||||
|
||||
Examples:
|
||||
+ `⏯:keyevent:85` A play/pause key (which has no effect in most apps).
|
||||
+ `my@:'my.email@domain.com'` A key that sends an arbitrary string
|
||||
|
||||
- A macro, `legend:key_def1,key_def2,...`.
|
||||
This results in a key with legend `legend` that behaves as if the sequence of `key_def` had been pressed in order.
|
||||
|
||||
Examples:
|
||||
+ `CA:ctrl,a,ctrl,c` A key with legend CA that sends the sequence `ctrl+a`, `ctrl+c`.
|
||||
+ `Cd:ctrl,backspace` A key with legend Cd that sends the shortcut `ctrl+backspace`.
|
||||
|
||||
### Escape codes
|
||||
|
||||
When defining a key value, several characters have special effects. If you want a character not to have its usual effect but to be taken literally, you should "escape" it in the usual way for XML:
|
||||
|
||||
To get this character... | ...you can type
|
||||
:---- | :------
|
||||
`\?` | `?`
|
||||
`\#` | `#`
|
||||
`\@` | `@`
|
||||
`\n` | Literal newline character. This is different from `enter` and `action` in certain apps.
|
||||
`\t` | Literal tab character. This is different from `tab` in certain apps.
|
||||
`\\` | `\`
|
||||
A literal newline character, which is different from `enter` and `action` in certain apps. | `\n`
|
||||
A literal tab character, which is different from `tab` in certain apps. | `\t`
|
||||
`&` | `&`
|
||||
`<` | `<`
|
||||
`>` | `>`
|
||||
`"` | `"`
|
||||
|
||||
XML escape codes also work, including:
|
||||
The characters `\` (unless followed by n or t), `?`, `#`, and `@` do not need to be escaped when writing custom layouts. When writing a layout to be included in the app (in [srcs/layouts](https://github.com/Julow/Unexpected-Keyboard/tree/master/srcs/layouts)), they are represented by typing `\\`, `\?`, `\#`, and `\@`.
|
||||
|
||||
Value | Escape code for
|
||||
:------- | :------
|
||||
`&` | `&`
|
||||
`<` | `<`
|
||||
`>` | `>`
|
||||
`"` | `"`
|
||||
The characters `,` and `:` can be escaped in a key value, using single quotes. For example, this macro defines a key with legend `http` that sends a string containing `:`: `<key c="http:home,'https://'" />` For simplicity, `,` and `:` cannot be escaped in the key legend.
|
||||
|
||||
## Modifiers
|
||||
System modifiers are sent to the app, which is free to do whatever they want in response.
|
||||
The other modifiers only exist within the keyboard.
|
||||
System modifiers are sent to the app, which can take app-specific action.
|
||||
|
||||
Value | Meaning
|
||||
:---------- | :------
|
||||
`shift` | System modifier.
|
||||
`ctrl` | System modifier.
|
||||
`alt` | System modifier.
|
||||
`meta` | System modifier. Equivalent to the Windows key.
|
||||
`fn` | Activates Fn mode, which assigns letters and symbols to special characters. e.g. `fn` `!` = `¡`
|
||||
`compose` | Compose key. Enables composing characters using Linux-like shortcuts; e.g. `Compose` `A` `single quote` types `Á` (A with acute accent).
|
||||
`capslock` | Actives and locks Shift
|
||||
|
||||
## Special keys
|
||||
The other modifiers take effect only within the keyboard.
|
||||
|
||||
Value | Meaning
|
||||
:---------- | :------
|
||||
`fn` | Activates Fn mode, which assigns letters and symbols to special characters. Example: `fn` `!` = `¡`
|
||||
`compose` | Compose key. Enables composing characters using Linux-like shortcuts. Example: `Compose` `A` `'` types `Á` (A with acute accent).
|
||||
`capslock` | Activates and locks Shift.
|
||||
|
||||
## App function keys
|
||||
These keys are sent to apps, which are free to ignore them. The keyboard does not perform editing in response to these keys.
|
||||
|
||||
`esc`, `enter`,
|
||||
@@ -53,6 +83,19 @@ These keys are sent to apps, which are free to ignore them. The keyboard does no
|
||||
`selectAll`, `pasteAsPlainText`,
|
||||
`undo`, `redo`
|
||||
|
||||
## Keyboard editing actions
|
||||
In contrast, these keys perform editing on the text without sending anything to the app.
|
||||
Value | Meaning
|
||||
:-------------------- | :------
|
||||
`cursor_left` | Moves the cursor to the left with the slider gesture.
|
||||
`cursor_right` | Moves the cursor to the right with the slider gesture.
|
||||
`cursor_up` | Moves the cursor up with the slider gesture. Warning: this might make the cursor leave the text box.
|
||||
`cursor_down` | Moves the cursor down with the slider gesture. Warning: this might make the cursor leave the text box.
|
||||
`delete_word` | Delete the word to the left of the cursor.
|
||||
`forward_delete_word` | Delete the word to the right of the cursor.
|
||||
|
||||
The values with `cursor_` are new in v1.31.0. Previous custom layouts specified the slider with `slider="true"`, which should be removed.
|
||||
|
||||
## Whitespace
|
||||
Value | Meaning
|
||||
:------ | :------
|
||||
@@ -62,13 +105,6 @@ Value | Meaning
|
||||
`zwj` | Zero-width joiner.
|
||||
`zwnj` | Zero-width non-joiner.
|
||||
|
||||
## Keyboard editing actions
|
||||
These keys perform editing on the text without sending keys that the app can interpret differently or ignore.
|
||||
Value | Meaning
|
||||
:----------------- | :------
|
||||
`cursor_left` | Moves the cursor position to the left directly, without sending a `left` key event.
|
||||
`cursor_right` | Moves the cursor position to the right directly, without sending a `right` key event.
|
||||
|
||||
## Other modifiers and diacritics
|
||||
Value | Meaning
|
||||
:------------------- | :------
|
||||
@@ -92,6 +128,7 @@ Value | Meaning
|
||||
`accent_horn` | Horn accent. `ơ`
|
||||
`accent_hook_above` | Hook accent. `ả`
|
||||
`accent_double_grave` | Double grave accent. `ȁ`
|
||||
`accent_small_caps` | Small capital letters. `ᴀ`
|
||||
`superscript` | Superscript. `ᵃ`
|
||||
`subscript` | Subscript. `ₐ`
|
||||
`ordinal` | Turns `a` and `o` into `ª` and `º`.
|
||||
@@ -105,6 +142,20 @@ Value | Meaning
|
||||
`rlm` | Right-to-left mark.
|
||||
`b(`, `b)`, `b[`, `b]`, `b{`, `b}`, `blt`, `bgt` | Sends the bracket characters, but with mirrored key legends for right-to-left languages. (`blt` and `bgt` print `<` and `>` respectively.)
|
||||
|
||||
## Arabic
|
||||
These are the diacritics for Arabic
|
||||
|
||||
Value | Meaning
|
||||
:------ | :------
|
||||
`combining_fatha` | َ
|
||||
`combining_dammah` | ُ
|
||||
`combining_kasra` | ِ
|
||||
`combining_sukun` | ْ
|
||||
`combining_shaddah` | ّ
|
||||
`combining_hamza_above` | ٔ
|
||||
`combining_hamza_below` | ٕ
|
||||
|
||||
|
||||
## Hebrew
|
||||
Keys ending in `_placeholder` are normally hidden unless the Fn key is pressed.
|
||||
|
||||
@@ -139,6 +190,16 @@ Value | Meaning
|
||||
`voice_typing` | Begin voice typing.
|
||||
`voice_typing_chooser` | Shows a menu where you can choose which voice typing provider to use, then begins voice typing when you make a selection.
|
||||
`shareText` | Emit a share Intent for the selected text. **Oddity:** This is in CamelCase.
|
||||
`hide_self` | Hide the keyboard.
|
||||
|
||||
## Suggestions
|
||||
|
||||
Value | Meaning
|
||||
:---- | :------
|
||||
`complete_first` | Enter the best suggestion (the one in the middle)
|
||||
`complete_second` | Enter the second best suggestion (the right one)
|
||||
`complete_third` | Enter the third best suggestion (the left one)
|
||||
`complete_emoji` | Enter the emoji suggestion
|
||||
|
||||
## Unused
|
||||
These keys are known to do nothing.
|
||||
@@ -150,50 +211,3 @@ These keys are known to do nothing.
|
||||
These keys are normally hidden unless the Fn modifier is activated.
|
||||
|
||||
`f11_placeholder` | `f12_placeholder`
|
||||
|
||||
## Complex keys
|
||||
|
||||
More complex keys are of this form:
|
||||
|
||||
```
|
||||
:<kind> <attributes>:<payload>
|
||||
```
|
||||
|
||||
Where `<kind>` is one of the kinds documented below and `<attributes>` is a
|
||||
space separated list of attributes. `<payload>` depends on the `<kind>`.
|
||||
|
||||
Attributes are:
|
||||
- `symbol='Sym'` specifies the symbol to be shown on the keyboard.
|
||||
- `flags='<flags>'` changes the behavior of the key.
|
||||
`<flags>` is a coma separated list of:
|
||||
+ `dim`: Make the symbol dimmer.
|
||||
+ `small`: Make the symbol smaller.
|
||||
|
||||
### Kind `str`
|
||||
|
||||
Defines a key that outputs an arbitrary string. `<payload>` is a string wrapped
|
||||
in single-quotes (`'`), escaping of other single quotes is allowed with `\'`.
|
||||
|
||||
For example:
|
||||
- `:str:'Arbitrary string with a \' inside'`
|
||||
- `:str symbol='Symbol':'Output string'`
|
||||
|
||||
### Kind `char`
|
||||
|
||||
Defines a key that outputs a single character. `<payload>` is the character to
|
||||
output, unquoted.
|
||||
This kind of key can be used to define a character key with a different symbol
|
||||
on it. `char` keys can be modified by `ctrl` and other modifiers, unlike `str`
|
||||
keys.
|
||||
|
||||
For example:
|
||||
- `:char symbol='љ':q`, which is used to implement `ctrl` shortcuts in cyrillic
|
||||
layouts.
|
||||
|
||||
### Kind `keyevent`
|
||||
|
||||
Defines a key that sends an Android [key event](https://developer.android.com/reference/android/view/KeyEvent).
|
||||
`<payload>` is the key event number.
|
||||
|
||||
For example:
|
||||
- `:keyevent symbol='⏯' flags='small':85`
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
الميزة الرئيسة هي إمكانية كتابة المزيد من الأحرف عن طريق تمرير المفاتيح نحو الزوايا.
|
||||
|
||||
صُمم هذا التطبيق في الأصل للمبرمجين الذين يستخدمون Termux.
|
||||
|
||||
أصبح الآن مثاليًا للاستخدام اليومي.
|
||||
@@ -0,0 +1 @@
|
||||
لوحة مفاتيح افتراضية خفيفة الوزن ومراعية للخصوصية لنظام أندرويد.
|
||||
@@ -0,0 +1 @@
|
||||
Unexpected Keyboard
|
||||
@@ -0,0 +1,6 @@
|
||||
La característica principal és que pots escriure més caràcters arrossegant les tecles cap a les cantonades.
|
||||
|
||||
Aquesta aplicació va ser dissenyada originalment per als programadors que usen Termux.
|
||||
Ara és perfecta per a un ús quotidià.
|
||||
|
||||
Aquesta aplicació no conté anuncis i és de codi obert.
|
||||
@@ -0,0 +1 @@
|
||||
Un teclat virtual lleuger per a Android que és conscient de la teva privacitat.
|
||||
@@ -0,0 +1 @@
|
||||
Unexpected Keyboard
|
||||
@@ -3,4 +3,4 @@ Hlavní funkcí je možnost psát více znaků posunutím kláves směrem k roh
|
||||
Tato aplikace byla původně navržena pro programátory používající Termux.
|
||||
Nyní je ideální pro každodenní použití.
|
||||
|
||||
Tato aplikace neobsahuje žádné reklamy, nevyužívá připojení k síti a je Open Source.
|
||||
Tato aplikace neobsahuje žádné reklamy a je Open Source.
|
||||
|
||||
@@ -3,4 +3,4 @@ Diese Tastatur zeichnet sich dadurch aus, dass man zusätzliche Zeichen durch Wi
|
||||
Die Anwendung wurde ursprünglich für das Programmieren in Termux entwickelt.
|
||||
Mittlerweile ist sie auch für den täglichen Gebrauch perfekt geeignet.
|
||||
|
||||
Diese App enthält keine Werbung, benötigt keinen Netzwerkzugriff und ist quelloffen.
|
||||
Diese App enthält keine Werbung und ist quelloffen.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Bug fixes
|
||||
@@ -0,0 +1,8 @@
|
||||
Delete a word with a circle gesture on the delete key.
|
||||
Compose key can be unselected.
|
||||
Improved space bar slider and added selection mode.
|
||||
Lock shift with a circle gesture.
|
||||
Improvements to layouts.
|
||||
Various bug fixes and improvements to themes.
|
||||
|
||||
Huge thanks to the contributors: @0skar2 @chuckwagoncomputing @dzaima @eandersons Lokesh Kumar @lrvideckis @ms-jagadeeshan @quantenzitrone @Sestowner @solokot @Spike-from-NH @srikanban @tenextractor
|
||||
@@ -0,0 +1,9 @@
|
||||
Bug fixes
|
||||
|
||||
Delete a word with a circle gesture on the delete key.
|
||||
Compose key can be unselected.
|
||||
Improved space bar slider and added selection mode.
|
||||
Lock shift with a circle gesture.
|
||||
Various bug fixes and improvements to themes and layouts.
|
||||
|
||||
Many thanks to the contributors: @HaleyHalcyon @Jaoheah @Spike-from-NH
|
||||
@@ -0,0 +1,6 @@
|
||||
Better support for foldable devices
|
||||
Added layouts
|
||||
Improved selection mode
|
||||
Bug fixes and many other improvements
|
||||
|
||||
Huge thanks to the contributors: @alotbsol555, @bergentroll, @dacc9, @HaleyHalcyon, @Jaoheah, @jorexdeveloper, @malbajun, @matejdro, @Spike-from-NH, @tenextractor
|
||||
@@ -0,0 +1 @@
|
||||
Bug fixes
|
||||
@@ -0,0 +1,6 @@
|
||||
Spell checking and autocorrect are here !
|
||||
|
||||
This releases comes with a very large number of improvements,
|
||||
support for more languages and new themes!
|
||||
|
||||
Huge thanks to the contributors: @10Develops @anaskaejdar @andyca111100 @Autissima @bqv @cillyvms @domportera @dzaima @exzombie @Harry-Ais0 @Houfalafel @JeelsBoobz @JFronny @Julow @lan700ng @malbajun @matejdro @n-ce @skynetcat @sperareine @Spike-from-NH @xxcavemanxx
|
||||
@@ -0,0 +1,6 @@
|
||||
Spell checking and autocorrect are here !
|
||||
|
||||
This releases comes with a very large number of improvements,
|
||||
support for many languages and new themes!
|
||||
|
||||
Huge thanks to the contributors: @10Develops @anaskaejdar @andyca111100 @Autissima @bqv @cillyvms @domportera @dzaima @exzombie @Harry-Ais0 @Houfalafel @JeelsBoobz @JFronny @Julow @lan700ng @malbajun @matejdro @n-ce @skynetcat @sperareine @Spike-from-NH @xxcavemanxx
|
||||
@@ -0,0 +1,3 @@
|
||||
Bug fixes
|
||||
|
||||
Huge thanks to the contributors: @alotbsol555 @FlippingBinary
|
||||
@@ -0,0 +1,6 @@
|
||||
Spell checking and autocorrect are here !
|
||||
|
||||
This releases comes with a very large number of improvements,
|
||||
support for many languages and new themes!
|
||||
|
||||
Huge thanks to the contributors: @10Develops @alotbsol555 @anaskaejdar @andyca111100 @Autissima @bqv @cillyvms @domportera @dzaima @exzombie @FlippingBinary @Harry-Ais0 @Houfalafel @JeelsBoobz @JFronny @Julow @lan700ng @malbajun @matejdro @n-ce @skynetcat @sperareine @Spike-from-NH @xxcavemanxx
|
||||
@@ -0,0 +1 @@
|
||||
Bug fixes
|
||||
@@ -3,4 +3,4 @@ The main feature is that you can type more characters by swiping the keys toward
|
||||
This application was originally designed for programmers using Termux.
|
||||
Now perfect for everyday use.
|
||||
|
||||
This application contains no ads, doesn't make any network requests and is Open Source.
|
||||
This application contains no ads and is Open Source.
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 115 KiB |
@@ -3,4 +3,4 @@ La característica principal es que hay acceso a más caractéres deslizando hac
|
||||
Esta aplicación fue originalmente diseñada para programadores que usaran Termux.
|
||||
Ahora es perfecta para uso cotidiano.
|
||||
|
||||
La misma no contiene ningún anuncio/publicidad, no realiza peticiones de red y es de Fuente Abierta.
|
||||
La misma no contiene ningún anuncio/publicidad y es de Fuente Abierta.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Peamine omadus on see, et saad sisestada rohkem märke, viibates klahve nurkade suunas.
|
||||
|
||||
See rakendus loodi algselt Termuxit kasutavatele programmeerijatele.
|
||||
Nüüd sobib see ideaalselt igapäevaseks kasutamiseks.
|
||||
|
||||
Rakendus ei sisalda reklaame ja on avatud lähtekoodiga.
|
||||
@@ -0,0 +1 @@
|
||||
Väike ja privaatsusteadlik klahvistik Androidile.
|
||||
@@ -0,0 +1 @@
|
||||
Ootamatu klahvistik
|
||||
@@ -0,0 +1,6 @@
|
||||
Ang pangunahing feature ay maaari kang mag-type ng mas marami pang karakter sa pag-swipe sa gilid ng mga key.
|
||||
|
||||
Noong simula, ginawa itong application para sa mga programmer sa na nagte-Termux.
|
||||
Ngayo'y perpekto na sa pang-araw-araw.
|
||||
|
||||
Walang ads at Open Source ang application na ito.
|
||||
@@ -0,0 +1 @@
|
||||
Magaan at privacy-conscious na virtual keyboard sa Android.
|
||||
@@ -0,0 +1 @@
|
||||
Unexpected Keyboard
|
||||
@@ -3,4 +3,4 @@ La fonctionnalité principale est l'accès rapide à plus de caractères en bala
|
||||
Cette application a été conçue à l'origine pour les programmeurs utilisant Termux.
|
||||
Elle est maintenant parfaite pour une utilisation quotidienne.
|
||||
|
||||
Cette application ne contient pas de publicité, n'accède pas au réseau et est Open Source.
|
||||
Cette application ne contient pas de publicité et est Open Source.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Fitur utamanya adalah Anda dapat mengetik lebih banyak karakter dengan menggeser tombol ke arah sudut.
|
||||
|
||||
Aplikasi ini awalnya dirancang untuk programmer yang menggunakan Termux.
|
||||
Sekarang sempurna untuk penggunaan sehari-hari.
|
||||
|
||||
Aplikasi ini tidak berisi iklan dan Open Source.
|
||||
@@ -0,0 +1 @@
|
||||
Keyboard virtual yang ringan dan sadar privasi untuk Android.
|
||||
@@ -0,0 +1 @@
|
||||
Unexpected Keyboard
|
||||
@@ -0,0 +1,6 @@
|
||||
La caratteristica principale è quella di poter digitare altri caratteri scorrendo verso i bordi dei tasti.
|
||||
|
||||
L'applicazione era originariamente pensata per l'utilizzo di Termux da parte di programmatori.
|
||||
Ora è perfetta anche per l'uso quotidiano.
|
||||
|
||||
L'applicazione non contiene pubblicità ed è Open Source.
|
||||
@@ -1 +1 @@
|
||||
Una Tastiera Virtuale Leggera Per La Programmazione
|
||||
Una tastiera virtuale leggera ed attenta alla privacy per Android.
|
||||
|
||||
@@ -3,5 +3,3 @@
|
||||
このアプリは元々はTermuxでのプログラミング用に設計されました。
|
||||
しかし、今では普段の入力にも適しています。
|
||||
PCキーボードでの半角入力を再現しています。日本語入力、変換は出来ません。
|
||||
|
||||
このアプリは広告を含まず、インターネットに接続せず、そしてオープンソースです。
|
||||
|
||||
@@ -2,5 +2,3 @@
|
||||
|
||||
이 앱은 처음에는 Termux를 사용하는 프로그래머들을 위한 것으로 개발되었습니다.
|
||||
지금은 일상적인 용도로도 완벽합니다.
|
||||
|
||||
이 응용 프로그램에는 광고가 없으며 네트워크 요청을 하지 않고 오픈 소스입니다.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Galvenā iezīme ir iespēja ievadīt vairāk rakstzīmju ar pavilkšanu uz taustiņu stūriem.
|
||||
Tastatūras galvenā iespēja — viegli ievadīt jebkuru ASCII rakstzīmi ar pavilkšanu uz taustiņu stūriem.
|
||||
|
||||
Šī lietotne sākotnēji tika izstrādāta programmētājiem, kas izmanto Termux.
|
||||
Tagad lieliski piemērota izmantošanai ikdienā.
|
||||
Sākotnēji šī lietotne tika izstrādāta programmētājiem, kas izmanto Termux.
|
||||
Tagad tā lieliski piemērota arī ikdienas lietošanai.
|
||||
|
||||
Šī lietotne nesatur reklāmas, neveic nekādus tīkla pieprasījumus, un tās pirmkods ir pieejams visiem.
|
||||
Lietotne nesatur reklāmas, un tās pirmkods ir pieejams visiem.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Mazizmēra un privātumu ievērojoša virtuālā Android tastatūra.
|
||||
Mazizmēra virtuālā Android tastatūra, kurai rūp privātums.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
De belangrijkste functie is dat je meer tekens kunt typen door de toetsen naar de hoeken te vegen.
|
||||
|
||||
Deze applicatie is oorspronkelijk ontworpen voor programmeurs die Termux gebruiken.
|
||||
Nu perfect voor dagelijks gebruik.
|
||||
|
||||
Deze applicatie bevat geen advertenties en is Open Source.
|
||||
@@ -0,0 +1 @@
|
||||
Lichtgewicht en privacy-bewust virtueel toetsenbord voor Android.
|
||||
@@ -0,0 +1 @@
|
||||
Unexpected Keyboard
|
||||
@@ -3,4 +3,4 @@ Główną cechą tej klawiatury jest możliwość wprowadzania więcej znaków p
|
||||
Ta aplikacja została pierwotnie zaprojektowana z myślą o programistach używających Termuxa.
|
||||
Obecnie nadaje się doskonale do codziennego użytku.
|
||||
|
||||
Aplikacja nie zawiera reklam, nie żąda dostępu do internetu, a jej kod źródłowy jest dostępny publicznie.
|
||||
Aplikacja nie zawiera reklam, a jej kod źródłowy jest dostępny publicznie.
|
||||
|
||||
@@ -3,4 +3,4 @@ A principal característica é que você pode digitar mais caracteres deslizando
|
||||
O app foi criado originalmente para desenvolvedores que usam Termux.
|
||||
Agora aperfeiçoado para o uso diário.
|
||||
|
||||
Este aplicativo não contém anúncios, não faz nenhuma solicitação de rede e é Open Source.
|
||||
Este aplicativo não contém anúncios e é Open Source.
|
||||
|
||||
@@ -3,4 +3,4 @@ Funcționalitatea principală este accesul rapid la o mulțime de caractere ASCI
|
||||
Această aplicație a fost concepută inițial pentru programatori care folosec Termux.
|
||||
Este perfectă pentru uzul cotidian.
|
||||
|
||||
Această aplicație nu conține publicitate, nu folosește rețeaua deloc și e Open Source.
|
||||
Această aplicație nu conține publicitate și e Open Source.
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
Приложение изначально было разработано для использования с Termux.
|
||||
На данный момент оно также удобно в повседневном использовании.
|
||||
|
||||
Приложение не содержит рекламы, не осуществляет никаких запросов в сеть и имеет открытый исходный код.
|
||||
Приложение не содержит рекламы и имеет открытый исходный код.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
คีย์บอร์ดนี้สามารถเขียนตัวอักษรได้หลายตัวมากขึ้นโดยใช้การปัดคีย์ไปที่มุมต่างๆ
|
||||
|
||||
คีย์บอร์ดนี้ถูกออกแบบมาเพื่อนักเขียนโปรแกรมที่ใช้เทอร์มักซ์ (Termux)
|
||||
แต่ตอนนี้เพอร์เฟ็กต์สำหรับคนทั่วไป
|
||||
@@ -0,0 +1 @@
|
||||
คีย์บอร์ดสำหรับแอนดรอยด์ที่กินทรัพยากรน้อยและโฟกัสที่ความเป็นส่วนตัว
|
||||
@@ -0,0 +1 @@
|
||||
Unexpected Keyboard
|
||||
@@ -3,4 +3,4 @@ Bu uygulama özünde tuşların kenarlarından kaydırarak daha fazla karakter y
|
||||
Bu uygulama aslında Termux kullanıcıları için geliştirildi.
|
||||
Artık gündelik kullanım için de uygun.
|
||||
|
||||
Bu uygulama açık kaynaklıdır. Reklam içermez ve internete bağlanmaz.
|
||||
Bu uygulama açık kaynaklıdır. Reklam içermez.
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
Ця програма спочатку була розроблена для програмістів, які використовують Termux.
|
||||
Тепер ідеально підходить для щоденного використання.
|
||||
|
||||
Ця програма не містить реклами, не надсилає жодних мережевих запитів і має відкритий код.
|
||||
Ця програма не містить реклами і має відкритий код.
|
||||
|
||||
@@ -3,4 +3,4 @@ Chức năng chính là dễ dàng gõ nhiều ký tự bằng cách kéo phím
|
||||
Ứng dụng này ban đầu được thiết kế cho các lập trình viên dùng Termux.
|
||||
Bây giờ đã hoàn hảo cho việc sử dụng hàng ngày.
|
||||
|
||||
Ứng dụng này không chứa quảng cáo, không cần đến mạng, và có mã nguồn mở.
|
||||
Ứng dụng này không chứa quảng cáo, có mã nguồn mở.
|
||||
|
||||
@@ -2,5 +2,3 @@
|
||||
|
||||
此应用最初是为使用 Termux 的程序员而设计的。
|
||||
现在对于日常使用来说也很完美。
|
||||
|
||||
此应用没有广告,不会发送任何网络请求,而且是开源的。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
你能透過往按鍵角落滑動來輸入更多字元。
|
||||
|
||||
該軟體原先被設計給Termux使用者使用。
|
||||
現在已完善並可日用。
|
||||
|
||||
此應用程式無任何廣告並且是開源的。
|
||||
@@ -0,0 +1 @@
|
||||
為Android打造的輕量、注重隱私的虛擬鍵盤。
|
||||
@@ -0,0 +1 @@
|
||||
Unexpected Keyboard
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"version": "v1.0.0",
|
||||
"entity": {
|
||||
"type": "individual",
|
||||
"role": "owner",
|
||||
"name": "Julow",
|
||||
"email": "jules@j3s.fr",
|
||||
"description": "Open source developer and maintainer of Unexpected Keyboard.",
|
||||
"webpageUrl": {
|
||||
"url": "https://github.com/Julow"
|
||||
}
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"guid": "unexpected-keyboard",
|
||||
"name": "Unexpected Keyboard",
|
||||
"description": "Lightweight and privacy-conscious virtual keyboard for Android.",
|
||||
"webpageUrl": {
|
||||
"url": "https://github.com/Julow/Unexpected-Keyboard/"
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"url": "https://github.com/Julow/Unexpected-Keyboard/"
|
||||
},
|
||||
"licenses": [
|
||||
"spdx:GPL-3.0",
|
||||
"spdx:CC0-1.0"
|
||||
],
|
||||
"tags": [
|
||||
"android",
|
||||
"mobile",
|
||||
"privacy",
|
||||
"productivity",
|
||||
"programming",
|
||||
"user-experience"
|
||||
]
|
||||
}
|
||||
],
|
||||
"funding": {
|
||||
"channels": [
|
||||
{
|
||||
"guid": "liberapay",
|
||||
"type": "other",
|
||||
"address": "https://liberapay.com/Julow/",
|
||||
"description": "Recurring donations for funding Unexpected Keyboard."
|
||||
},
|
||||
{
|
||||
"guid": "github-sponsors",
|
||||
"type": "other",
|
||||
"address": "https://github.com/sponsors/Julow",
|
||||
"description": "Recurring donations for funding Unexpected Keyboard."
|
||||
},
|
||||
{
|
||||
"guid": "paypal",
|
||||
"type": "other",
|
||||
"address": "https://paypal.me/JulesAguillon",
|
||||
"description": "One-time donations for funding Unexpected-keyboard."
|
||||
}
|
||||
],
|
||||
"plans": [
|
||||
{
|
||||
"guid": "fund-maintenance",
|
||||
"status": "active",
|
||||
"name": "Fund developer time",
|
||||
"description": "Help the maintainers spend time on Unexpected Keyboard",
|
||||
"amount": 0,
|
||||
"currency": "EUR",
|
||||
"frequency": "monthly",
|
||||
"channels": [
|
||||
"liberapay",
|
||||
"github-sponsors",
|
||||
"paypal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"guid": "one-time-contribution",
|
||||
"status": "active",
|
||||
"name": "Fund developer time",
|
||||
"description": "Help the maintainers spend time on Unexpected Keyboard",
|
||||
"amount": 0,
|
||||
"currency": "EUR",
|
||||
"frequency": "one-time",
|
||||
"channels": [
|
||||
"liberapay",
|
||||
"github-sponsors",
|
||||
"paypal"
|
||||
]
|
||||
}
|
||||
],
|
||||
"history": []
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,8 @@ def generate_arrays(out, layouts):
|
||||
root.append(mk_array("string-array", "pref_layout_entries", entries_items))
|
||||
root.append(mk_array("integer-array", "layout_ids", ids_items))
|
||||
XML.indent(root)
|
||||
XML.ElementTree(element=root).write(out, encoding="unicode", xml_declaration=True)
|
||||
XML.ElementTree(element=root).write(out, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
layouts = sort_layouts(read_layouts(glob.glob("srcs/layouts/*.xml")))
|
||||
with open("res/values/layouts.xml", "w") as out:
|
||||
generate_arrays(out, layouts)
|
||||
with open("res/values/layouts.xml", "wb") as out:
|
||||
generate_arrays(out, layouts)
|
||||
@@ -0,0 +1,189 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
import itertools as it
|
||||
import sys
|
||||
|
||||
# This script generates res/xml/method.xml.
|
||||
|
||||
def warn(msg):
|
||||
print("Warning: " + msg, file=sys.stderr)
|
||||
|
||||
def loc(loc_name, script, default_layout, **kwargs):
|
||||
return { "name": loc_name, "script": script,
|
||||
"default_layout": default_layout, **kwargs }
|
||||
|
||||
# The locales are defined here. To add support for a language, add it to the
|
||||
# following block:
|
||||
|
||||
LOCALES = [
|
||||
loc("ar", "arabic", "arab_pc_hindu"),
|
||||
loc("ar_TN", "arabic", "arab_pc"),
|
||||
loc("as", "beng", "beng_assamese"),
|
||||
loc("az_AZ", "latin", "latn_qwerty_az", extra_keys="accent_trema:ü:ö@w|accent_cedille:ç:ş@s|ğ@g|ı@k|ə@l"),
|
||||
loc("be_BY", "cyrillic", "cyrl_jcuken_ru", extra_keys="ґ|є|і|ї|ў"),
|
||||
loc("bg_BG", "cyrillic", "cyrl_ueishsht", extra_keys="€"),
|
||||
loc("bn_BD", "latin", "latn_qwerty_us", extra_keys="৳"),
|
||||
loc("bs", "latin", "latn_qwerty_us", extra_keys="đ|ž|lj|nj|ć|č|dž|š"),
|
||||
loc("ca", "latin", "latn_qwerty_us", extra_keys="accent_grave:à:ò:è|accent_cedille:ç@c|accent_aigu:é:í:ú:ó|accent_trema:ï:ü|ŀl|€"),
|
||||
loc("cs_CZ", "latin", "latn_qwertz_cz", extra_keys="accent_aigu:á:é:í:ó:ú:ý@d|accent_ring:ů@s|accent_caron:č:ě:ň:ř:š:ž:ď:ť@f"),
|
||||
loc("cy_GB", "latin", "latn_qwerty_cy"),
|
||||
loc("da_DK", "latin", "latn_qwerty_da", extra_keys="€|æ|å|ø"),
|
||||
loc("de_BE", "latin", "latn_azerty_be", extra_keys="accent_grave:è@f|accent_aigu:á:é:í:ó:ú:ý:j́@d|accent_circonflexe:ê@f|accent_cedille:ç@c|accent_trema@u|€"),
|
||||
loc("de_CH", "latin", "latn_qwertz_de", extra_keys="accent_trema:ä:ö:ü@u|ß"),
|
||||
loc("de_DE", "latin", "latn_qwertz_de", extra_keys="accent_trema:ä:ö:ü@u|ß|€"),
|
||||
loc("el", "latin", "grek_qwerty", extra_keys="£@l|€"),
|
||||
loc("en", "latin", "latn_qwerty_us", dictionary="en_GB"),
|
||||
loc("en_AU", "latin", "latn_qwerty_us"),
|
||||
loc("en_CA", "latin", "latn_qwerty_us", dictionary="en_US"),
|
||||
loc("en_GB", "latin", "latn_qwerty_gb", extra_keys="£@l"),
|
||||
loc("en_IN", "latin", "latn_qwerty_us"),
|
||||
loc("en_NG", "latin", "latn_qwerty_us", extra_keys="₦"),
|
||||
loc("en_US", "latin", "latn_qwerty_us"),
|
||||
loc("es_ES", "latin", "latn_qwerty_es", extra_keys="accent_aigu:á:é:í:ó:ú@d|accent_tilde:ñ@n|accent_grave@f|accent_trema@u|€"),
|
||||
loc("et_EE", "latin", "latn_qwerty_et", extra_keys="accent_trema:ä:ö:ü@u|accent_tilde:õ@o|accent_caron:š:ž@s|€"),
|
||||
loc("eu", "latin", "latn_qwerty_us", extra_keys="ñ|ç|ü|dd|ll|rr|ts|tt|tx|tz"),
|
||||
loc("fa_IR", "persian", "arab_pc_ir"),
|
||||
loc("fi", "latin", "latn_qwerty_fi", extra_keys="å|accent_ring|accent_aigu|accent_trema|ö|ä|€"),
|
||||
loc("fr_BE", "latin", "latn_azerty_be", extra_keys="accent_grave:à:è:ù@f|accent_aigu:é@d|accent_circonflexe:ê:û@f|accent_cedille:ç@c|accent_trema@u|€"),
|
||||
loc("fr_CA", "latin", "latn_azerty_fr", extra_keys="accent_grave:à:è:ù@f|accent_aigu:é@d|accent_circonflexe:â:ê:ô:û@f|accent_cedille:ç@c|accent_trema:ë:ï:ü:ÿ@u"),
|
||||
loc("fr_CH", "latin", "latn_qwertz_fr_ch", extra_keys="accent_grave:à:è:ù@f|accent_aigu:é@d|accent_circonflexe:â:ê:ô:û@o|accent_cedille:ç@c|accent_trema:ë:ï:ü:ÿ@u|€"),
|
||||
loc("fr_FR", "latin", "latn_azerty_fr", extra_keys="accent_grave:à:è:ù@d|accent_aigu:é@d|accent_circonflexe:â:ê:ô:û@o|accent_cedille:ç@c|accent_trema:ë:ï:ü@l|€"),
|
||||
loc("ga_IE", "latin", "latn_qwerty_ga", extra_keys="accent_aigu:á:é:í:ó:ú@k|accent_dot_above@l"),
|
||||
loc("gl", "latin", "latn_qwerty_us"),
|
||||
loc("ha_NG", "latin", "latn_qwerty_us", extra_keys="₦|ɓ|ɗ|ƙ|’|ƴ|r̃"),
|
||||
loc("haw_US", "latin", "latn_qwerty_haw", extra_keys="ʻ@l|accent_macron:ā:ē:ī:ō:ū@m"),
|
||||
loc("he_IL", "hebrew", "hebr_1_il", extra_keys="₪@r"),
|
||||
loc("hi_IN", "devanagari", "deva_inscript", extra_keys="₹"),
|
||||
loc("hr", "latin", "latn_qwerty_us", extra_keys="č|ć|dž|đ|lj|nj|š|ž"),
|
||||
loc("hu_HU", "latin", "latn_qwertz_hu", extra_keys="accent_aigu:á:é:í:ó:ú@d|accent_trema:ö:ü@u|accent_ogonek@s|accent_double_aigu:ő:ű@k|€"),
|
||||
loc("hy", "armenian", "armenian_ph_am"),
|
||||
loc("ig_NG", "latin", "latn_qwerty_us", extra_keys="₦|ṅ|ọ|ụ"),
|
||||
loc("is_IS", "latin", "latn_qwerty_is", extra_keys="ð|þ|æ|accent_trema:ö@o|accent_aigu:á:é:í:ó:ú:ý@d|accent_circonflexe|accent_ring|accent_grave"),
|
||||
loc("it_IT", "latin", "latn_qwerty_us", extra_keys="accent_grave:à:è:ì:ò:ù@f|accent_aigu:é:ó@d|accent_circonflexe:î@f|€|ə"),
|
||||
loc("ka_GE", "latin", "georgian_mes"),
|
||||
loc("kk_KZ", "latin", "cyrl_jcuken_kk"),
|
||||
loc("kn_IN", "kannada", "kann_kannada"),
|
||||
loc("ko_KR", "hangul", "hang_dubeolsik_kr"),
|
||||
loc("lb", "latin", "latn_qwerty_us", extra_keys="é|ä|ë|accent_grave|accent_cedille@c|accent_aigu|accent_trema|€"),
|
||||
loc("lt_LT", "latin", "latn_qwerty_lt", extra_keys="accent_ogonek:ą:ę:į:ų@s|accent_caron:č:š:ž@f|accent_dot_above:ė@s|accent_macron:ū@o|€"),
|
||||
loc("lv_LV", "latin", "latn_qwerty_lv", extra_keys="accent_macron:ā:ē:ī:ū@o|accent_caron:č:š:ž@f|accent_ogonek:ķ:ļ:ņ@s|accent_cedille:ģ@c|€"),
|
||||
loc("mk", "cyrillic", "cyrl_lynyertdz_mk", extra_keys="ѕ|ѓ|ќ|ѝ|ѐ|љ|њ|џ|„|“|€"),
|
||||
loc("mn_MN", "cyrillic", "cyrl_fcuzhen_mn", extra_keys="ү|ө"),
|
||||
loc("mr_IN", "devanagari", "deva_inscript", extra_keys="₹"),
|
||||
loc("mt_MT", "latin", "latn_qwerty_mt", extra_keys="accent_grave:à:è:ì:ò:ù|accent_dot_above:ċ:ż:ġ|ħ"),
|
||||
loc("nb", "latin", "latn_qwerty_us", extra_keys="€|æ@a|å@a|ø@o|accent_aigu:é:ó@d|accent_grave:è:ò:ù@f|accent_circonflexe:ê:ô@f"),
|
||||
loc("ne_NE", "devanagari", "deva_inscript", extra_keys="₹"),
|
||||
loc("nl_BE", "latin", "latn_azerty_be", extra_keys="accent_grave:è@f|accent_aigu:á:é:í:ó:ú:ý:j́@d|accent_circonflexe:ê@f|accent_cedille:ç@c|accent_trema@u|€"),
|
||||
loc("no_NO", "latin", "latn_qwerty_us", extra_keys="€|æ@a|å@a|ø@o|accent_aigu:é:ó@d|accent_grave:è:ò:ù@f|accent_circonflexe:ê:ô@f"),
|
||||
loc("pl_PL", "latin", "latn_qwerty_pl"),
|
||||
loc("pt_BR", "latin", "latn_qwerty_pt", extra_keys="accent_aigu:á:é:í:ó:ú@d|accent_cedille:ç@c|accent_circonflexe:â:ê:ô@f|accent_grave:à:ò@f|accent_tilde:ã:õ@n|€|ª|º"),
|
||||
loc("pt_PT", "latin", "latn_qwerty_pt", extra_keys="accent_aigu:á:é:í:ó:ú@d|accent_cedille:ç@c|accent_circonflexe:â:ê:ô@f|accent_grave:à:ò@f|accent_tilde:ã:õ@n|€|ª|º"),
|
||||
loc("ro_RO", "latin", "latn_qwerty_ro", extra_keys="ă|â|î|ș|ț|€|$"),
|
||||
loc("ru_RU", "latin", "cyrl_jcuken_ru"),
|
||||
loc("si_LK", "sinhala", "sinhala_phonetic", extra_keys="₨"),
|
||||
loc("sk_SK", "latin", "latn_qwertz_sk", extra_keys="accent_caron:ě:ř:ž:š:č:ň:ď:ľ:ť@f|accent_ring:ů@s|accent_circonflexe:ô@f|accent_trema:ä:ü:ö@u|accent_aigu:á:é:í:ó:ú:ŕ:ś:ĺ:ý@d"),
|
||||
loc("sl", "latin", "latn_qwerty_us", extra_keys="accent_caron:Č:Š:Ž|€"),
|
||||
loc("sq_AL", "latin", "latn_qwertz_sq"),
|
||||
loc("sr_", "latin", "cyrl_lynyertz_sr"),
|
||||
loc("sv_SE", "latin", "latn_qwerty_se", extra_keys="accent_aigu:á@d|accent_trema:ä:ö@o|accent_ring:å@s|€"),
|
||||
loc("ta_IN", "tamil", "tamil_default"),
|
||||
loc("tly_AZ", "latin", "latn_qwerty_tly", extra_keys="á|ú|â|ê|ı|š|ž"),
|
||||
loc("tly_IR", "persian", "arab_hamvaj_tly"),
|
||||
loc("tr_TR", "latin", "latn_qwerty_tr", extra_keys="accent_cedille:ç:ş@c|accent_trema:ö:ü@u|accent_circonflexe:â:î:û@f|₺|ı|ğ"),
|
||||
loc("uk_UA", "cyrillic", "cyrl_jcuken_uk", extra_keys="ґ|є|і|ї|₴"),
|
||||
loc("ur", "persian", "arab_pc_ir"),
|
||||
loc("uz_UZ", "latin", "latn_qwerty_uz", extra_keys="ʻ|ʼ"),
|
||||
loc("vi_VN", "latin", "latn_qwerty_vi"),
|
||||
loc("yo_NG", "latin", "latn_qwerty_us", extra_keys="₦|ẹ|ọ|ṣ")
|
||||
]
|
||||
|
||||
# The locale that is at the beginning of the list.
|
||||
DEFAULT_LOCALE = "en_GB"
|
||||
|
||||
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()
|
||||
|
||||
# Warn when a dictionary is attached to no locale
|
||||
def check_locales_for_dictionaries(locales):
|
||||
used = { l["dictionary"] for l in locales if "dictionary" in l }
|
||||
for d in available_dictionaries:
|
||||
if d not in used:
|
||||
warn("Dictionary '%s' is attached to no locale" % d)
|
||||
|
||||
def subtype_elem(root, loc):
|
||||
tag = loc["tag"].replace("_", "-")
|
||||
extra_keys = ",extra_keys=" + loc["extra_keys"] if "extra_keys" in loc else ""
|
||||
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 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]
|
||||
def find_locale(name):
|
||||
l = name.split("_")[0]
|
||||
return next(( loc for loc in locales_grouped[l] if loc["name"] == name ), None)
|
||||
for loc in LOCALES:
|
||||
locales_grouped.setdefault(lang(loc), []).append(loc)
|
||||
def tag(loc):
|
||||
l = lang(loc)
|
||||
if loc["name"] == l: return l # Locales like "en"
|
||||
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"]
|
||||
def dictionary(loc):
|
||||
if loc is None: return None
|
||||
if "dictionary" in loc: return loc["dictionary"]
|
||||
if loc["name"] in available_dictionaries: return loc["name"]
|
||||
l = lang(loc)
|
||||
if l in available_dictionaries: return l
|
||||
if l != loc["name"]: return dictionary(find_locale(l))
|
||||
return None
|
||||
def add_attrs(loc):
|
||||
loc = dict(**loc)
|
||||
loc["tag"] = tag(loc)
|
||||
loc["dictionary"] = dictionary(loc)
|
||||
return loc
|
||||
return map(add_attrs, LOCALES)
|
||||
|
||||
def sort_locales(locales):
|
||||
# The default locale for a language (eg. "en") might shadow the exact
|
||||
# locale (eg. "en_US"). Makes sure the default locale sorts after the exact
|
||||
# ones.
|
||||
def key(l):
|
||||
s = l["name"].split("_")
|
||||
return (l["name"] != DEFAULT_LOCALE), s[0], (len(s) == 1), s[1:]
|
||||
return sorted(locales, key=key)
|
||||
|
||||
def gen():
|
||||
locales = sort_locales(compute_attrs())
|
||||
check_locales_for_dictionaries(locales)
|
||||
root = ET.Element("input-method", attrib={
|
||||
"xmlns:android": "http://schemas.android.com/apk/res/android",
|
||||
"android:settingsActivity": "juloo.mornkeyboard2.SettingsActivity",
|
||||
"android:supportsSwitchingToNextInputMethod": "true",
|
||||
})
|
||||
root.append(ET.Comment(text=""" This file is automatically generated. DO NOT EDIT.
|
||||
Locales definitions should go into 'gen_method_xml.py'.
|
||||
Update this file with 'gradle test'.
|
||||
|
||||
"""))
|
||||
for loc in locales:
|
||||
subtype_elem(root, loc)
|
||||
ET.indent(root)
|
||||
print(ET.tostring(root, encoding="utf-8", xml_declaration=True).decode("UTF-8"))
|
||||
|
||||
gen()
|
||||
@@ -0,0 +1,524 @@
|
||||
#! /bin/env python3
|
||||
|
||||
"""
|
||||
Script to generate a layout based on an existing.
|
||||
|
||||
Tuned to create Sinhala phonetic layout based on qwerty (US), but may be adoped
|
||||
for other scripts. Look at dicts before the LayoutBuilder code.
|
||||
|
||||
Usage:
|
||||
python3 gen_sinhala_phonetic_layout [-h|--help] [-v|--verbose] [-o|--output]
|
||||
|
||||
By default with no args will write to corresponding file in `srcs/layouts/`.
|
||||
|
||||
Script uses central symbol (in direction "c") to identify a key, which may not
|
||||
be appropriate for base (reference) layouts other, than qwerty (US).
|
||||
|
||||
Warning will be printed to stderr if new symbol overrides some symbol of the
|
||||
reference layout in directions other, than "c".
|
||||
|
||||
Exception will be rised on other
|
||||
conflicts e. g. when trying to move a symbol into occupied position.
|
||||
|
||||
- Made with latn_qwerty_us.xml from commit `6b1551d`
|
||||
- Made with Python 3.13
|
||||
- Requires Python >= 3.11
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
class Placement(StrEnum):
|
||||
C = 'c'
|
||||
NW = 'nw'
|
||||
N = 'n'
|
||||
NE = 'ne'
|
||||
E = 'e'
|
||||
SE = 'se'
|
||||
S = 's'
|
||||
SW = 'sw'
|
||||
W = 'w'
|
||||
|
||||
|
||||
# Based on XKB Sinhala (phonetic)
|
||||
KEYS_MAP: dict[str, tuple[str, str, str, str]] = {
|
||||
# Row 1 ###########################################
|
||||
'q': ('ඍ', 'ඎ', '\u0DD8', '\u0DF2'),
|
||||
'w': ('ඇ', 'ඈ', '\u0DD0', '\u0DD1'),
|
||||
'e': ('එ', 'ඒ', '\u0DD9', '\u0DDA'),
|
||||
'r': ('ර', '', '', ''), # In XKB virama is on layer 2
|
||||
't': ('ත', 'ථ', 'ට', 'ඨ'),
|
||||
'y': ('ය', '', '', ''), # In XKB virama is on layer 2
|
||||
'u': ('උ', 'ඌ', '\u0DD4', '\u0DD6'),
|
||||
'i': ('ඉ', 'ඊ', '\u0DD2', '\u0DD3'),
|
||||
'o': ('ඔ', 'ඕ', '\u0DDC', '\u0DDD'),
|
||||
'p': ('ප', 'ඵ', '', ''),
|
||||
# Row 2 ###########################################
|
||||
'a': ('අ', 'ආ', '\u0DCA', '\u0DCF'),
|
||||
's': ('ස', 'ශ', 'ෂ', ''),
|
||||
'd': ('ද', 'ධ', 'ඩ', 'ඪ'),
|
||||
'f': ('ෆ', '\u0D93', '', '\u0DDB'), # In XKB aiyanna placed otherwise
|
||||
'g': ('ග', 'ඝ', 'ඟ', ''),
|
||||
'h': ('හ', '\u0D83', '\u0DDE', 'ඖ'),
|
||||
'j': ('ජ', 'ඣ', 'ඦ', ''),
|
||||
'k': ('ක', 'ඛ', 'ඦ', 'ඐ'),
|
||||
'l': ('ල', 'ළ', '\u0DDF', '\u0DF3'),
|
||||
# Row 3 ###########################################
|
||||
'z': ('ඤ', 'ඥ', '', ''), # In XKB contains bar, broken bar
|
||||
'x': ('ඳ', 'ඬ', '', ''),
|
||||
'c': ('ච', 'ඡ', '', ''),
|
||||
'v': ('ව', '', '', ''),
|
||||
'b': ('බ', 'භ', '', ''),
|
||||
'n': ('න', 'ණ', '\u0D82', 'ඞ'),
|
||||
'm': ('ම', 'ඹ', '', ''),
|
||||
}
|
||||
|
||||
# How to place four levels of Key.
|
||||
# Syntax: LEVEL: PLACEMENT | 'FROM_LEVEL+MODIFIER'
|
||||
# The last means symbol on level FROM_LEVEL with modifier key MODIFIER gives
|
||||
# key on level LEVEL
|
||||
#
|
||||
LEVELS_MAP = {
|
||||
0: Placement.C,
|
||||
1: Placement.NE,
|
||||
2: '0+shift',
|
||||
3: '1+shift',
|
||||
}
|
||||
|
||||
# Additional modify keys combinations.
|
||||
# Syntax:
|
||||
# MODKEY: { A: B }
|
||||
#
|
||||
MODMAP_EXTRA: dict[str, dict[str, str]] = {
|
||||
'shift': {
|
||||
# Astrological numbers
|
||||
'1': '෧',
|
||||
'2': '෨',
|
||||
'3': '෩',
|
||||
'4': '෪',
|
||||
'5': '෫',
|
||||
'6': '෬',
|
||||
'7': '෭',
|
||||
'8': '෮',
|
||||
'9': '෯',
|
||||
'0': '෦',
|
||||
# Kunddaliya
|
||||
'.': '෴',
|
||||
# Extra broken bar intead z key in XKB
|
||||
'\u007C': '\u00A6',
|
||||
# Special whitespaces
|
||||
'zwj': 'zwnj',
|
||||
},
|
||||
'fn': {
|
||||
# Sinhala archaic digits
|
||||
'ඍ': '𑇡', # 1
|
||||
'ඇ': '𑇢', # 2
|
||||
'එ': '𑇣', # 3
|
||||
'ර': '𑇤', # 4
|
||||
'ත': '𑇥', # 5
|
||||
'ය': '𑇦', # 6
|
||||
'උ': '𑇧', # 7
|
||||
'ඉ': '𑇨', # 8
|
||||
'ඔ': '𑇩', # 9
|
||||
'ප': '𑇪', # 10
|
||||
'අ': '𑇫', # 20
|
||||
'ස': '𑇬', # 30
|
||||
'ද': '𑇭', # 40
|
||||
'ෆ': '𑇮', # 50
|
||||
'ග': '𑇯', # 60
|
||||
'හ': '𑇰', # 70
|
||||
'ජ': '𑇱', # 80
|
||||
'ක': '𑇲', # 90
|
||||
'ල': '𑇳', # 100
|
||||
'ළ': '𑇴', # 1000
|
||||
# Sinhala candrabindu for Sanskrit
|
||||
'ණ': '\u0D81',
|
||||
},
|
||||
}
|
||||
|
||||
# Table to move additional characters in reference layout.
|
||||
# Format is (CENTRAL_CHAR, PLACEMENT): (CENTRAL_CHAR, PLACEMENT). E. g. to move
|
||||
# char from key with central character "q", direction "se" to key with central
|
||||
# character "w", direction "sw", add line:
|
||||
# ('q', Placement.SE): ('w', Placement.SW),
|
||||
#
|
||||
# To delete a char, use None as destination placement. E.g.:
|
||||
# ('q', Placment.SE): ('q', None)
|
||||
#
|
||||
# Moving of main char in central placement is not supported.
|
||||
#
|
||||
TRANSITIONS_MAP: dict[tuple[str, Placement], tuple[str, Placement | None]] = {
|
||||
('q', Placement.SE): ('q', Placement.SW), # loc esc
|
||||
('q', Placement.NE): ('q', Placement.SE), # 1
|
||||
|
||||
('w', Placement.NE): ('w', Placement.SE), # 2
|
||||
|
||||
('e', Placement.SE): ('r', Placement.NW), # loc €
|
||||
('e', Placement.NE): ('e', Placement.SE), # 3
|
||||
|
||||
('r', Placement.NE): ('r', Placement.SE), # 4
|
||||
('t', Placement.NE): ('t', Placement.SE), # 5
|
||||
('y', Placement.NE): ('y', Placement.SE), # 6
|
||||
('u', Placement.NE): ('u', Placement.SE), # 7
|
||||
('i', Placement.NE): ('i', Placement.SE), # 8
|
||||
|
||||
('o', Placement.SE): ('p', Placement.SW), # )
|
||||
('o', Placement.NE): ('o', Placement.SE), # 9
|
||||
|
||||
('p', Placement.NE): ('p', Placement.SE), # 0
|
||||
|
||||
('a', Placement.NE): ('a', Placement.NW), # `
|
||||
('a', Placement.NW): ('a', Placement.SW), # loc tab
|
||||
|
||||
('s', Placement.NE): ('s', Placement.NW), # loc §
|
||||
|
||||
('g', Placement.SW): ('g', Placement.NW), # _
|
||||
('g', Placement.NE): ('g', Placement.SW), # -
|
||||
|
||||
('h', Placement.SW): ('h', Placement.NW), # +
|
||||
('h', Placement.NE): ('h', Placement.SW), # =
|
||||
|
||||
('l', Placement.NE): ('l', Placement.NW), # |
|
||||
|
||||
('x', Placement.NE): ('x', Placement.NW), # loc †
|
||||
('c', Placement.NE): ('c', Placement.NW), # <
|
||||
('b', Placement.NE): ('b', Placement.NW), # ?
|
||||
('n', Placement.NE): ('n', Placement.NW), # :
|
||||
('m', Placement.NE): ('m', Placement.NW), # "
|
||||
}
|
||||
|
||||
# Add additional characters to arbitrary places.
|
||||
# Syntax is CHAR: POSITION, where POSITION is a pari as in TRANSITIONS_MAP.
|
||||
#
|
||||
CHARS_EXTRA = {
|
||||
# In XKB ZWJ is on `/` key, and ZWNJ is on spacebar
|
||||
'zwj': ('m', Placement.SE),
|
||||
}
|
||||
|
||||
|
||||
# List of char unicode numbers and inclusive ranges of numbers to encode as XML
|
||||
# numeric character references.
|
||||
# Good for combining signs to not mess with quotes.
|
||||
# Characters in line of the keyboard tag will not be escaped.
|
||||
#
|
||||
ESCAPE_LIST: list[int | tuple[int, int]] = [
|
||||
# Sinhalese diacritics
|
||||
(0xD81, 0xD83),
|
||||
(0xDCA, 0xDDF),
|
||||
]
|
||||
|
||||
# Default filename. Output path can be overrided with `-o` flag also.
|
||||
LAYOUT_FILENAME = 'sinhala_phonetic.xml'
|
||||
|
||||
# Will be placed after XML declaration. Need to have proper <!-- --> tags.
|
||||
COMMENT = '''
|
||||
<!-- This file defines Sinhala layout.
|
||||
|
||||
Based on XKB Sinhala (phonetic) layout.
|
||||
-->
|
||||
'''
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
REFERENCE_LAYOUT_FILE = BASE_DIR / 'srcs/layouts/latn_qwerty_us.xml'
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
KeysMapType = list[list[ElementTree.Element]]
|
||||
|
||||
|
||||
class LayoutGenError(RuntimeError):
|
||||
...
|
||||
|
||||
|
||||
def xml_elem_to_str(element: ElementTree.Element) -> str:
|
||||
return ElementTree.tostring(
|
||||
element,
|
||||
xml_declaration=False,
|
||||
encoding='unicode').strip()
|
||||
|
||||
|
||||
def keys_map_to_str(keys_map: KeysMapType) -> str:
|
||||
""" Make laout rows map printable for debug purposes """
|
||||
result = '[\n'
|
||||
for row in keys_map:
|
||||
result += ' ' * 4
|
||||
for key in row:
|
||||
result += str(key.attrib) + ', '
|
||||
result += '\n'
|
||||
result += ']'
|
||||
return result
|
||||
|
||||
|
||||
def is_in_escape_list(char: str | int) -> bool:
|
||||
if isinstance(char, str):
|
||||
char = ord(char)
|
||||
for item in ESCAPE_LIST:
|
||||
if isinstance(item, tuple) and char >= item[0] and char <= item[1]:
|
||||
return True
|
||||
elif isinstance(item, int):
|
||||
if char == item:
|
||||
return True
|
||||
else:
|
||||
TypeError(f'Unexpected item {item} of ESCAPE_LIST')
|
||||
return False
|
||||
|
||||
|
||||
def xml_encode_char(ch: str | int) -> str:
|
||||
if isinstance(ch, str):
|
||||
ch = ord(ch)
|
||||
hex_val = hex(ch).split('x')[-1]
|
||||
return f'&#x{hex_val.upper().zfill(4)};'
|
||||
|
||||
|
||||
class LayoutBuilder:
|
||||
XML_DECLARATION = "<?xml version='1.0' encoding='utf-8'?>"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = '',
|
||||
script: str = '',
|
||||
numpad_script: str = '',
|
||||
comment: str = '',
|
||||
) -> None:
|
||||
"""
|
||||
:param comment: MUST be a valid XML comment wrapped in <!-- tags -->
|
||||
"""
|
||||
attrs = {}
|
||||
if name:
|
||||
attrs['name'] = name
|
||||
if script:
|
||||
attrs['script'] = script
|
||||
if numpad_script:
|
||||
attrs['numpad_script'] = numpad_script
|
||||
self._comment = None
|
||||
if comment:
|
||||
self._comment = comment.strip() or None
|
||||
self._xml_keyboard = ElementTree.Element('keyboard', attrib=attrs)
|
||||
self._modmap = ElementTree.Element('modmap')
|
||||
|
||||
@staticmethod
|
||||
def _parse_reference_layout() -> list[ElementTree.Element]:
|
||||
return ElementTree.parse(REFERENCE_LAYOUT_FILE).findall('row')
|
||||
|
||||
@staticmethod
|
||||
def _move_untransited_to_new_map(
|
||||
ref_map: KeysMapType,
|
||||
new_map: KeysMapType
|
||||
) -> None:
|
||||
coordinates = [
|
||||
(row_num, key_num)
|
||||
for row_num in range(len(ref_map))
|
||||
for key_num in range(len(ref_map[row_num]))
|
||||
]
|
||||
|
||||
for row_num, key_num in coordinates:
|
||||
old_key = ref_map[row_num][key_num]
|
||||
new_key = new_map[row_num][key_num]
|
||||
for k, val in old_key.attrib.items():
|
||||
if (transited := new_key.attrib.get(k)) is not None:
|
||||
msg = (
|
||||
f'Transition of {transited} to'
|
||||
f' {new_key.get(Placement.C)}:{k} conflictls with'
|
||||
f' existing value "{val}"')
|
||||
raise LayoutGenError(msg)
|
||||
new_key.set(k, val)
|
||||
|
||||
@staticmethod
|
||||
def _add_extra_chars_to_ref_map(
|
||||
coord_map: dict[str, tuple[int, int]],
|
||||
new_map: KeysMapType
|
||||
) -> None:
|
||||
for char, (to_key_name, to_plc) in CHARS_EXTRA.items():
|
||||
if not (to_coord := coord_map.get(to_key_name)):
|
||||
msg = f'Trying to add "{char}" to missing key "{to_key_name}"'
|
||||
raise LayoutGenError(msg)
|
||||
row_num, key_num = to_coord
|
||||
key = new_map[row_num][key_num]
|
||||
if (existing := key.get(to_plc)) is not None:
|
||||
msg = f'Trying to add char to <{to_key_name}:{to_plc}>, but already contains "{existing}"'
|
||||
raise LayoutGenError(msg)
|
||||
key.set(to_plc, char)
|
||||
LOGGER.info(
|
||||
'Added "%s" to <%s:%s>',
|
||||
char, to_key_name, to_plc)
|
||||
|
||||
@classmethod
|
||||
def _apply_transitions(cls, ref_map: list) -> list:
|
||||
coord_map: dict[str, tuple[int, int]] = {}
|
||||
|
||||
coordinates = [
|
||||
(row_num, key_num)
|
||||
for row_num in range(len(ref_map))
|
||||
for key_num in range(len(ref_map[row_num]))
|
||||
]
|
||||
|
||||
for row_num, key_num in coordinates:
|
||||
row = ref_map[row_num]
|
||||
key = row[key_num]
|
||||
key_name = key.get(Placement.C)
|
||||
if key_name in coord_map:
|
||||
msg = f'Duplicated value "{key_name}" in central position'
|
||||
raise LayoutGenError(msg)
|
||||
coord_map[key_name] = (row_num, key_num)
|
||||
|
||||
# Make new map with empty keys
|
||||
result_map = [[ElementTree.Element('key') for key in row] for row in ref_map]
|
||||
|
||||
# Place by transitions map on new places
|
||||
for (from_key_name, from_plc), (to_key_name, to_plc) in TRANSITIONS_MAP.items():
|
||||
if Placement.C in (from_plc, to_plc):
|
||||
raise NotImplementedError('Transition from or to placment "c"')
|
||||
if not (from_coord := coord_map.get(from_key_name)):
|
||||
raise LayoutGenError(f'Transition from missing key {from_key_name}')
|
||||
if not (to_coord := coord_map.get(to_key_name)):
|
||||
raise LayoutGenError(f'Transition to missing key {to_key_name}')
|
||||
from_key = ref_map[from_coord[0]][from_coord[1]]
|
||||
to_key = result_map[to_coord[0]][to_coord[1]]
|
||||
try:
|
||||
val = from_key.attrib.pop(from_plc)
|
||||
except KeyError:
|
||||
msg = f'No value in key {from_key_name}, placement {from_plc} to move'
|
||||
raise LayoutGenError(msg)
|
||||
if to_plc is not None:
|
||||
if to_key.get(to_plc):
|
||||
msg = f'Second transition to key {to_key_name}, placement {to_plc}'
|
||||
raise LayoutGenError(msg)
|
||||
to_key.set(to_plc, val)
|
||||
LOGGER.info(
|
||||
'Moved "%s" from <%s:%s> to <%s:%s>',
|
||||
val, from_key_name, from_plc, to_key_name, to_plc)
|
||||
else:
|
||||
LOGGER.info(
|
||||
'Deleted "%s" from <%s:%s>',
|
||||
val, from_key_name, from_plc)
|
||||
|
||||
# Fill new map with other values
|
||||
cls._move_untransited_to_new_map(ref_map, new_map=result_map)
|
||||
|
||||
# Add additional characters
|
||||
cls._add_extra_chars_to_ref_map(coord_map, new_map=result_map)
|
||||
|
||||
return result_map
|
||||
|
||||
@staticmethod
|
||||
def _resolve_placement(
|
||||
key: ElementTree.Element,
|
||||
placement: Placement,
|
||||
new_char: str
|
||||
) -> None:
|
||||
if placement != Placement.C:
|
||||
central_char = key.get(Placement.C)
|
||||
existing = key.get(placement)
|
||||
if existing:
|
||||
LOGGER.warning(
|
||||
'Placement %s of key %s already occupied with %s',
|
||||
placement, central_char, existing)
|
||||
key.set(placement, new_char)
|
||||
|
||||
def _process_key(self, key: ElementTree.Element) -> ElementTree.Element:
|
||||
central_char = key.get(Placement.C)
|
||||
if not central_char:
|
||||
return key
|
||||
new_key_entry = KEYS_MAP.get(central_char)
|
||||
if new_key_entry is None:
|
||||
return key
|
||||
|
||||
for level, placement_spec in LEVELS_MAP.items():
|
||||
if not (new_char := new_key_entry[level]):
|
||||
continue
|
||||
if '+' in placement_spec:
|
||||
pair = placement_spec.split('+')
|
||||
from_level, modkey = int(pair[0]), pair[1]
|
||||
key_a = new_key_entry[from_level]
|
||||
key_b = new_char
|
||||
if key_a is None:
|
||||
raise LayoutGenError(f'Tried to modife {key_a} to {key_b}')
|
||||
ElementTree.SubElement(self._modmap, modkey, a=key_a, b=key_b)
|
||||
else:
|
||||
placement = Placement(placement_spec)
|
||||
self._resolve_placement(key, placement=placement, new_char=new_char)
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def _make_extra_modmap(modmap: ElementTree.Element) -> ElementTree.Element:
|
||||
for modkey, ab_map in MODMAP_EXTRA.items():
|
||||
for a, b in ab_map.items():
|
||||
LOGGER.info('Adding modmap %s "%s" -> "%s"', modkey, a, b)
|
||||
ElementTree.SubElement(modmap, modkey, a=a, b=b)
|
||||
return modmap
|
||||
|
||||
@staticmethod
|
||||
def _post_escape(data: str) -> str:
|
||||
buf = ''
|
||||
lines = data.splitlines(keepends=True)
|
||||
for line in lines:
|
||||
# Skip keyboard tag line to keep attributes
|
||||
if '<keyboard ' in line:
|
||||
buf += line
|
||||
continue
|
||||
for ch in line:
|
||||
if is_in_escape_list(ch):
|
||||
ch = xml_encode_char(ch)
|
||||
buf += ch
|
||||
return buf
|
||||
|
||||
def build(self) -> None:
|
||||
raw_ref_rows = self._parse_reference_layout()
|
||||
ref_rows = self._apply_transitions(raw_ref_rows)
|
||||
for row in ref_rows:
|
||||
new_row = ElementTree.SubElement(self._xml_keyboard, 'row')
|
||||
for key in row:
|
||||
LOGGER.debug(
|
||||
'Processing reference entry %s',
|
||||
xml_elem_to_str(key))
|
||||
new_row.append(self._process_key(key))
|
||||
self._modmap = self._make_extra_modmap(self._modmap)
|
||||
self._xml_keyboard.append(self._modmap)
|
||||
|
||||
def get_xml(self) -> str:
|
||||
ElementTree.indent(self._xml_keyboard)
|
||||
body_raw = xml_elem_to_str(self._xml_keyboard)
|
||||
body = self._post_escape(body_raw)
|
||||
|
||||
result = self.XML_DECLARATION + '\n'
|
||||
if self._comment:
|
||||
result += self._comment + '\n'
|
||||
result += body + '\n'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='gen_sinhala_phonetic_layout',
|
||||
description='Generate XKB-based Sinhala layout',)
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
'--output',
|
||||
default=BASE_DIR / f'srcs/layouts/{LAYOUT_FILENAME}',
|
||||
help='File to write result, `-` for stdout')
|
||||
parser.add_argument(
|
||||
'-v',
|
||||
'--verbose',
|
||||
help='More verbose logging',
|
||||
action='store_true')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = get_args()
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if args.verbose else logging.WARNING,
|
||||
format='%(levelname)s: %(message)s')
|
||||
builder = LayoutBuilder(name='සිංහල', script='sinhala', comment=COMMENT)
|
||||
builder.build()
|
||||
content = builder.get_xml()
|
||||
if args.output == '-':
|
||||
print(content)
|
||||
else:
|
||||
with open(args.output, 'w') as file:
|
||||
file.write(content)
|
||||
@@ -1,3 +1,4 @@
|
||||
org.gradle.jvmargs=-Dfile.encoding=UTF-8
|
||||
android.useAndroidX=false
|
||||
android.nonTransitiveRClass=true
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
org.gradle.configuration-cache=true
|
||||
@@ -1,6 +1,7 @@
|
||||
#Mon Aug 21 18:13:41 CEST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,81 +15,115 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +27,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to libswipetype are documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
---
|
||||
|
||||
## [0.1.0-dev] — 2026-02-26
|
||||
|
||||
Initial release — Phases 1–11 complete.
|
||||
|
||||
### Added
|
||||
- **swipetype-core** — C++17 gesture recognition library
|
||||
- `PathProcessor` — deduplicate, resample (64 points), bounding-box normalize
|
||||
- `IdealPathGenerator` — reference path generation with caching
|
||||
- `Scorer` — DTW distance with Sakoe-Chiba band (W=6)
|
||||
- `DictionaryLoader` — binary `.glide` format reader
|
||||
- `GestureEngine` — recognition pipeline orchestrator
|
||||
- `AdjacencyMap` — key adjacency computation (unused in scoring, for future use)
|
||||
- 48 Google Test unit tests (all passing)
|
||||
|
||||
- **swipetype-android** — Android AAR module
|
||||
- `SwipeTypeEngine` — Java lifecycle manager
|
||||
- `SwipeTypeAdapter` — keyboard integration interface
|
||||
- `GesturePoint`, `KeyboardLayoutDescriptor`, `SwipeTypeCandidate`, `SwipeTypeError` — data types
|
||||
- `GestureLibJNI.cpp` — JNI bridge
|
||||
|
||||
- **adapters/heliboard** — reference adapter for HeliBoard keyboard
|
||||
- `HeliboardSwipeTypeAdapter` — translates HeliBoard's `ProximityInfo`/`InputPointers` to swipetype API
|
||||
|
||||
- **sample-app** — minimal Input Method Service demo
|
||||
- `SampleKeyboardView` — custom QWERTY keyboard renderer with gesture trail
|
||||
- `SampleInputMethodService` — IME integration using `SwipeTypeAdapter`
|
||||
- `MainActivity` — setup guide with IME enable/switch buttons
|
||||
|
||||
- **scripts/gen_dict.py** — TSV-to-`.glide` dictionary generator
|
||||
- **test-data/** — 302-word English dictionary, QWERTY layout JSON, gesture scenarios
|
||||
|
||||
- **Documentation (Phase 9)**
|
||||
- `docs/API.md` — full C++ and Java API reference
|
||||
- `docs/ARCHITECTURE.md` — system architecture, pipeline diagram, design decisions
|
||||
- `docs/ONBOARDING.md` — developer onboarding guide (build, test, run)
|
||||
- `docs/HOW_TO_WRITE_AN_ADAPTER.md` — step-by-step adapter integration guide
|
||||
- `CHANGELOG.md` (this file)
|
||||
|
||||
- **Sample App Theme Fix (Phase 10)**
|
||||
- `styles.xml` with `AppTheme` and `SwipeTypeImeTheme`
|
||||
- Transparent window background prevents system theme bleed-through
|
||||
|
||||
- **Structural Accuracy Fixes (Phase 11)** — 3 algorithmic improvements
|
||||
- Key-transition word length estimation (replaced arc-length heuristic)
|
||||
- Absolute DTW normalization floor for single-candidate sets
|
||||
- Adaptive frequency weight: `effectiveAlpha *= max(0.1, rawRange/0.5)`
|
||||
- 3 new regression tests
|
||||
|
||||
- **CI/CD** — GitHub Actions workflows
|
||||
- `ci.yml` — Core C++ tests, Android build, NDK ABI matrix (arm64, armv7, x86_64)
|
||||
- `release.yml` — tag-triggered AAR release
|
||||
|
||||
### Fixed (Phase 8 — BUG-1 through BUG-6)
|
||||
- BUG-1: Dictionary loading moved from `onInit()` callback to `onCreate()` (eliminated infinite loop)
|
||||
- BUG-2: Keyboard layout now uses even key distribution
|
||||
- BUG-3: Dark theme with explicit colors (hardcoded in `SampleKeyboardView`)
|
||||
- BUG-4: `maxDTWFloor = 3.0` for single-candidate normalization
|
||||
- BUG-5: Frequency weight set to `α = 0.30` to balance shape vs. frequency
|
||||
- BUG-6: Dictionary expanded from 10 to 302 words
|
||||
@@ -0,0 +1,99 @@
|
||||
# Contributing to libswipetype
|
||||
|
||||
Thank you for your interest in contributing to the open-source libswipetype!
|
||||
|
||||
## Branch Naming Convention
|
||||
|
||||
All branches must follow this naming pattern:
|
||||
|
||||
| Prefix | Usage | Example |
|
||||
|--------|-------|---------|
|
||||
| `feature/` | New functionality | `feature/dtw-scoring` |
|
||||
| `fix/` | Bug fixes | `fix/path-normalization-crash` |
|
||||
| `adapter/` | Adapter work | `adapter/heliboard-jni-bridge` |
|
||||
| `docs/` | Documentation only | `docs/onboarding-guide` |
|
||||
| `test/` | Test additions/fixes | `test/scorer-edge-cases` |
|
||||
| `refactor/` | Code restructuring | `refactor/engine-pimpl` |
|
||||
|
||||
Branch names use lowercase with hyphens. No underscores, no camelCase.
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- `feat` — New feature
|
||||
- `fix` — Bug fix
|
||||
- `docs` — Documentation changes
|
||||
- `test` — Adding or fixing tests
|
||||
- `build` — Build system changes (CMake, Gradle, CI)
|
||||
- `refactor` — Code restructuring without behavior change
|
||||
- `perf` — Performance improvement
|
||||
- `chore` — Maintenance tasks
|
||||
|
||||
**Scopes:** `core`, `android`, `heliboard`, `sample`, `ci`, `docs`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
feat(core): implement DTW scoring with Sakoe-Chiba band
|
||||
fix(android): prevent JNI crash on null gesture path
|
||||
docs(heliboard): add integration guide for HeliBoard v2.x
|
||||
test(core): add edge case tests for single-point paths
|
||||
build(ci): add arm64-v8a to CI build matrix
|
||||
```
|
||||
|
||||
## Module Ownership
|
||||
|
||||
| Module | Primary Owner | Review Required For |
|
||||
|--------|---------------|---------------------|
|
||||
| `swipetype-core/include/swipetype/` | Developer A | **All changes** (joint review) |
|
||||
| `swipetype-core/src/` | Developer A | Internal — single review OK |
|
||||
| `swipetype-core/tests/` | Developer A | Internal — single review OK |
|
||||
| `swipetype-android/` (API interfaces) | Developer B | **Interface changes** (joint review) |
|
||||
| `swipetype-android/` (internals) | Developer B | Internal — single review OK |
|
||||
| `adapters/heliboard/` | Developer B | Single review OK |
|
||||
| `sample-app/` | Developer B | Single review OK |
|
||||
| `.github/workflows/` | Both | **All changes** (joint review) |
|
||||
|
||||
### Stable API Rule
|
||||
|
||||
Files in `swipetype-core/include/swipetype/` and the `SwipeTypeAdapter.java` / `SwipeTypeEngine.java` interfaces are **stable API surfaces**. Any changes to these files require:
|
||||
|
||||
1. A PR with the `api-change` label
|
||||
2. Approval from both developers
|
||||
3. A rationale comment explaining why the change is necessary
|
||||
4. Updated documentation in `docs/API.md`
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Create a branch following the naming convention
|
||||
2. Make your changes with conventional commit messages
|
||||
3. Ensure all tests pass locally (`scripts/run_tests.sh`)
|
||||
4. Push and open a PR using the template
|
||||
5. Request review from the appropriate owner(s)
|
||||
6. Address review feedback
|
||||
7. Squash-merge when approved
|
||||
|
||||
## Development Setup
|
||||
|
||||
See [docs/ONBOARDING.md](docs/ONBOARDING.md) for complete setup instructions.
|
||||
|
||||
## Clean-Room Notice
|
||||
|
||||
This project observes HeliBoard's JNI call signatures to ensure compatibility but does NOT copy any HeliBoard implementation code. All algorithms and implementations are original work. When documenting HeliBoard interface compatibility:
|
||||
|
||||
- Reference only public method signatures (names, parameter types, return types)
|
||||
- Never copy implementation logic from HeliBoard or AOSP LatinIME
|
||||
- Document the source of any interface information as "observed from public API"
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the Apache License 2.0.
|
||||
@@ -0,0 +1,177 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship made available under
|
||||
the License, as indicated by a copyright notice that is included in
|
||||
or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which
|
||||
the editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and its Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean, as submitted to the Licensor for inclusion
|
||||
in the Work by the copyright owner or by an individual or Legal Entity
|
||||
authorized to submit on behalf of the copyright owner. For the purposes
|
||||
of this definition, "submitted" means any form of electronic, verbal,
|
||||
or written communication sent to the Licensor or its representatives,
|
||||
including but not limited to communication on electronic mailing lists,
|
||||
source code control systems, and issue tracking systems that are managed
|
||||
by, or on behalf of, the Licensor for the purpose of recording and
|
||||
discussing the Work, but excluding communication that is conspicuously
|
||||
marked or designated in writing by the copyright owner as "Not a
|
||||
Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any Legal Entity on behalf of
|
||||
whom a Contribution has been received by the Licensor and included
|
||||
within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by the combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a cross-claim
|
||||
or counterclaim in a lawsuit) alleging that the Work or any
|
||||
Contribution embodied within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under
|
||||
this License for that Work shall terminate as of the date such
|
||||
litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative
|
||||
Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, You must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, in
|
||||
at least one of the following places: within a NOTICE text
|
||||
file distributed as part of the Derivative Works; within
|
||||
the Source form or documentation, if provided along with the
|
||||
Derivative Works; or, within a display generated by the
|
||||
Derivative Works, if and wherever such third-party notices
|
||||
normally appear. The contents of the NOTICE file are for
|
||||
informational purposes only and do not modify the License.
|
||||
You may add Your own attribution notices within Derivative
|
||||
Works that You distribute, alongside or in addition to the
|
||||
NOTICE text from the Work, provided that such additional
|
||||
attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own license statement for Your modifications and
|
||||
may provide additional grant of rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the
|
||||
Contribution, either alone or together with such modifications.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or reproducing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or exemplary damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (even if such Contributor has been advised of the possibility
|
||||
of such damages).
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may offer only
|
||||
conditions consistent with this License.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2024 libswipetype Contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,180 @@
|
||||
# libswipetype
|
||||
|
||||
An open-source, keyboard-agnostic glide (swipe) typing engine.
|
||||
|
||||
**libswipetype** provides accurate gesture-based word recognition for soft keyboards on Android. The core algorithm is written in portable C++17 with no external dependencies. An Android JNI wrapper and a reference HeliBoard adapter are included.
|
||||
|
||||
> **Status:** Pre-release — actively under development.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Keyboard App (HeliBoard, custom, …) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Adapter Layer (Java) │
|
||||
│ adapters/heliboard/ ←── SwipeTypeAdapter iface │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ swipetype-android (JNI bridge) │
|
||||
│ SwipeTypeEngine.java ↔ GestureLibJNI.cpp │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ swipetype-core (pure C++17) │
|
||||
│ PathProcessor → IdealPathGen → Scorer │
|
||||
│ DictionaryLoader GestureEngine │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Layers
|
||||
|
||||
| Layer | Language | Purpose |
|
||||
|-------|----------|---------|
|
||||
| **swipetype-core** | C++17 | Gesture recognition algorithms, dictionary loading |
|
||||
| **swipetype-android** | Java + JNI | Android library wrapping swipetype-core |
|
||||
| **adapters/heliboard** | Java + JNI | HeliBoard-specific integration adapter |
|
||||
| **sample-app** | Java | Minimal test keyboard app |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- CMake 3.18+
|
||||
- C++17 compiler (GCC 9+ or Clang 10+)
|
||||
- Android Studio Hedgehog+ (for Android modules)
|
||||
- Android NDK r25+ (installed via SDK Manager)
|
||||
|
||||
### Build & Test — Core Library (Desktop)
|
||||
|
||||
```bash
|
||||
cd swipetype-core
|
||||
mkdir build && cd build
|
||||
cmake .. -DSWIPETYPE_BUILD_TESTS=ON
|
||||
cmake --build . -j$(nproc)
|
||||
ctest --output-on-failure
|
||||
```
|
||||
|
||||
### Build — Android Library
|
||||
|
||||
```bash
|
||||
./gradlew :swipetype-android:assembleRelease
|
||||
```
|
||||
|
||||
The AAR is produced at `swipetype-android/build/outputs/aar/`.
|
||||
|
||||
### Build — HeliBoard Adapter
|
||||
|
||||
```bash
|
||||
./gradlew :adapters:heliboard:assembleRelease
|
||||
```
|
||||
|
||||
### Generate a Dictionary
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_dict.py test-data/en-us-sample.tsv test-data/en-us-sample.glide --lang en-US
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
### Using the Android Library
|
||||
|
||||
```java
|
||||
import dev.dettmer.swipetype.android.*;
|
||||
|
||||
// 1. Create engine & adapter
|
||||
SwipeTypeEngine engine = new SwipeTypeEngine();
|
||||
MyAdapter adapter = new MyAdapter(); // implements SwipeTypeAdapter
|
||||
|
||||
// 2. Initialize and load dictionary
|
||||
engine.init(context, adapter);
|
||||
InputStream dictStream = context.getResources().openRawResource(R.raw.en_us_sample);
|
||||
engine.loadDictionary("en-US", dictStream);
|
||||
|
||||
// 3. Recognize gestures (results delivered via adapter callback)
|
||||
List<GesturePoint> points = collectTouchPoints();
|
||||
engine.processGesture(points);
|
||||
|
||||
// In your adapter's onCandidatesReady():
|
||||
@Override
|
||||
public void onCandidatesReady(List<SwipeTypeCandidate> candidates) {
|
||||
String word = candidates.get(0).word; // public field
|
||||
float confidence = candidates.get(0).confidence; // public field
|
||||
}
|
||||
```
|
||||
|
||||
### Writing a Custom Adapter
|
||||
|
||||
Implement `SwipeTypeAdapter`:
|
||||
|
||||
```java
|
||||
public class MyKeyboardAdapter implements SwipeTypeAdapter {
|
||||
@Override
|
||||
public void onInit(SwipeTypeEngine engine) {
|
||||
// Engine is ready — load dictionary, etc.
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyboardLayoutDescriptor getKeyboardLayout() {
|
||||
// Return your keyboard's key positions (in dp)
|
||||
return new KeyboardLayoutDescriptor("en-US", keys, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCandidatesReady(List<SwipeTypeCandidate> candidates) {
|
||||
// Display candidates to the user
|
||||
String topWord = candidates.get(0).word;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(SwipeTypeError error) {
|
||||
Log.e("Adapter", "Error: " + error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [docs/HOW_TO_WRITE_AN_ADAPTER.md](docs/HOW_TO_WRITE_AN_ADAPTER.md) for a full guide.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
libswipetype/
|
||||
├── swipetype-core/ # Pure C++17 engine
|
||||
│ ├── include/swipetype/ # Public headers
|
||||
│ ├── src/ # Implementation
|
||||
│ └── tests/ # Google Test suite
|
||||
├── swipetype-android/ # Android JNI wrapper
|
||||
│ ├── src/main/java/ # Java API
|
||||
│ └── src/main/cpp/ # JNI bridge
|
||||
├── adapters/heliboard/ # HeliBoard reference adapter
|
||||
├── sample-app/ # Minimal test app
|
||||
├── scripts/ # gen_dict.py, run_tests.sh
|
||||
├── test-data/ # Sample dictionaries & gesture scenarios
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
## Algorithm Overview
|
||||
|
||||
1. **Preprocess** — Deduplicate → resample to 64 evenly-spaced points → normalize to [0, 1]
|
||||
2. **Filter** — Select dictionary words matching the gesture's start key, end key, and estimated length
|
||||
3. **Generate ideal paths** — For each candidate word, generate the "ideal" gesture path through key centers
|
||||
4. **Score** — Compare gesture path against ideal paths using Dynamic Time Warping (DTW) with Sakoe-Chiba band constraint
|
||||
5. **Rank** — Combine DTW distance with word frequency: `finalScore = (1 − α) · shapeScore + α · freqScore`
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Metric | Budget |
|
||||
|--------|--------|
|
||||
| Recognition latency | < 50ms on Snapdragon 665 |
|
||||
| Memory (50k words) | < 30 MB |
|
||||
| Init time | < 200ms |
|
||||
| Minimum Android API | 21 (Lollipop) |
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, branch naming, and code review process.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [Apache License 2.0](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
<p align="center">Made with ❤️ by <a href="https://github.com/inventory69">inventory69</a> & <a href="https://github.com/Hyphonical">Hyphonical</a></p>
|
||||
@@ -0,0 +1,456 @@
|
||||
# API Reference — libswipetype
|
||||
|
||||
> **Version:** 0.1.0 | **Language:** C++17 (core) & Java (Android wrapper) | **Updated:** 2026-02
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [C++ Core API](#c-core-api)
|
||||
- [GestureEngine](#gestureengine)
|
||||
- [RawGesturePath / GesturePath](#rawgesturepath--gesturepath)
|
||||
- [GestureCandidate](#gesturecandidate)
|
||||
- [KeyboardLayout / KeyDescriptor](#keyboardlayout--keydescriptor)
|
||||
- [ScoringConfig](#scoringconfig)
|
||||
- [DictionaryLoader](#dictionaryloader)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Constants](#constants)
|
||||
2. [Android (Java) API](#android-java-api)
|
||||
- [SwipeTypeEngine](#swipetypeengine)
|
||||
- [SwipeTypeAdapter](#swipetypeadapter)
|
||||
- [GesturePoint (Java)](#gesturepoint-java)
|
||||
- [KeyboardLayoutDescriptor](#keyboardlayoutdescriptor)
|
||||
- [SwipeTypeCandidate (Java)](#swipetypecandidate-java)
|
||||
- [SwipeTypeError](#swipetypeerror)
|
||||
|
||||
---
|
||||
|
||||
## C++ Core API
|
||||
|
||||
All C++ types live in the `swipetype` namespace.
|
||||
Header root: `swipetype-core/include/swipetype/`
|
||||
|
||||
### GestureEngine
|
||||
|
||||
**Header:** `GestureEngine.h`
|
||||
|
||||
The main entry point for gesture recognition. Orchestrates the full pipeline:
|
||||
normalize → filter → score → rank → return.
|
||||
|
||||
```cpp
|
||||
class GestureEngine {
|
||||
public:
|
||||
GestureEngine();
|
||||
~GestureEngine();
|
||||
|
||||
// Move-only (non-copyable)
|
||||
GestureEngine(GestureEngine&&) noexcept;
|
||||
GestureEngine& operator=(GestureEngine&&) noexcept;
|
||||
|
||||
bool init(const KeyboardLayout& layout, const std::string& dictPath);
|
||||
bool initWithData(const KeyboardLayout& layout,
|
||||
const uint8_t* dictData, size_t dictSize);
|
||||
std::vector<GestureCandidate> recognize(const RawGesturePath& rawPath,
|
||||
int maxCandidates = 8);
|
||||
void shutdown();
|
||||
bool isInitialized() const;
|
||||
bool updateLayout(const KeyboardLayout& layout);
|
||||
void configure(const ScoringConfig& config);
|
||||
void setErrorCallback(ErrorCallback callback);
|
||||
ErrorInfo getLastError() const;
|
||||
};
|
||||
```
|
||||
|
||||
#### `init(layout, dictPath) → bool`
|
||||
|
||||
Initialize the engine with a keyboard layout and a path to a binary `.glide` dictionary file.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `layout` | `const KeyboardLayout&` | Keyboard layout with key positions in dp |
|
||||
| `dictPath` | `const std::string&` | Absolute file path to the `.glide` dictionary |
|
||||
|
||||
**Returns:** `true` on success. On failure, call `getLastError()` for details.
|
||||
|
||||
**Post-condition:** `isInitialized() == true` on success.
|
||||
|
||||
#### `initWithData(layout, dictData, dictSize) → bool`
|
||||
|
||||
Initialize the engine with a keyboard layout and an in-memory dictionary.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `layout` | `const KeyboardLayout&` | Keyboard layout |
|
||||
| `dictData` | `const uint8_t*` | Pointer to raw dictionary bytes |
|
||||
| `dictSize` | `size_t` | Size of dictionary data in bytes |
|
||||
|
||||
**Returns:** `true` on success.
|
||||
|
||||
#### `recognize(rawPath, maxCandidates) → vector<GestureCandidate>`
|
||||
|
||||
Run the recognition pipeline on a raw gesture path.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `rawPath` | `const RawGesturePath&` | — | Raw touch points (≥ 2 points) |
|
||||
| `maxCandidates` | `int` | `8` | Max results. Clamped to [1, 20] |
|
||||
|
||||
**Returns:** Candidates sorted by confidence descending (best first). Empty if engine not initialized, path too short, or no matches found.
|
||||
|
||||
**Pipeline steps:**
|
||||
1. Deduplicate → resample to 64 points → bounding-box normalize
|
||||
2. Determine start/end key characters from first/last touch point
|
||||
3. Filter dictionary by start+end character, then by estimated word length
|
||||
4. Generate ideal path for each candidate word and compute DTW distance
|
||||
5. Normalize DTW scores, apply adaptive frequency weighting
|
||||
6. Sort by confidence and return top N
|
||||
|
||||
#### `shutdown()`
|
||||
|
||||
Release all resources (dictionary, caches). `isInitialized()` returns `false` after this call.
|
||||
|
||||
#### `updateLayout(layout) → bool`
|
||||
|
||||
Hot-swap the keyboard layout (e.g., after device rotation or language switch).
|
||||
Invalidates the ideal path cache. Does not reload the dictionary.
|
||||
|
||||
#### `configure(config)`
|
||||
|
||||
Override scoring parameters. See [ScoringConfig](#scoringconfig).
|
||||
|
||||
#### `setErrorCallback(callback)`
|
||||
|
||||
Register a callback for error notifications. Called synchronously from the thread that encounters the error.
|
||||
|
||||
```cpp
|
||||
engine.setErrorCallback([](const ErrorInfo& err) {
|
||||
std::cerr << "Error " << static_cast<int>(err.code) << ": " << err.message << "\n";
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### RawGesturePath / GesturePath
|
||||
|
||||
**Header:** `GesturePath.h`
|
||||
|
||||
```cpp
|
||||
// Raw input from the keyboard view
|
||||
struct RawGesturePath {
|
||||
std::vector<GesturePoint> points; // dp coordinates, ordered by time
|
||||
bool isEmpty() const; // true if < 2 points
|
||||
size_t size() const;
|
||||
};
|
||||
|
||||
// After normalization (64 points in [0,1] bounding box)
|
||||
struct GesturePath {
|
||||
std::vector<NormalizedPoint> points; // exactly RESAMPLE_COUNT (64)
|
||||
float aspectRatio; // originalWidth / originalHeight
|
||||
float totalArcLength; // in dp, before normalization
|
||||
int32_t startKeyIndex; // index into KeyboardLayout::keys
|
||||
int32_t endKeyIndex; // index into KeyboardLayout::keys
|
||||
bool isValid() const; // true if points.size() == 64
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GestureCandidate
|
||||
|
||||
**Header:** `GestureCandidate.h`
|
||||
|
||||
```cpp
|
||||
struct GestureCandidate {
|
||||
std::string word; // UTF-8 word
|
||||
float confidence; // [0.0, 1.0] — 1.0 = best
|
||||
uint32_t sourceFlags; // bitmask: SOURCE_MAIN_DICT, SOURCE_USER_DICT, etc.
|
||||
float dtwScore; // raw DTW distance (lower = better) — for debugging
|
||||
float frequencyScore; // normalized frequency [0.0, 1.0] — for debugging
|
||||
};
|
||||
```
|
||||
|
||||
**Source flags:**
|
||||
|
||||
| Constant | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| `SOURCE_MAIN_DICT` | `0x01` | From the primary dictionary |
|
||||
| `SOURCE_USER_DICT` | `0x02` | From the user dictionary (future) |
|
||||
| `SOURCE_COMPLETION` | `0x04` | Prefix completion (future) |
|
||||
|
||||
---
|
||||
|
||||
### KeyboardLayout / KeyDescriptor
|
||||
|
||||
**Header:** `KeyboardLayout.h`
|
||||
|
||||
```cpp
|
||||
struct KeyDescriptor {
|
||||
std::string label; // e.g. "a", "shift" — display/debug only
|
||||
int32_t codePoint; // Unicode code point. -1 for non-character keys
|
||||
float centerX; // key center X in dp
|
||||
float centerY; // key center Y in dp
|
||||
float width; // key width in dp
|
||||
float height; // key height in dp
|
||||
bool isCharacterKey() const;
|
||||
};
|
||||
|
||||
struct KeyboardLayout {
|
||||
std::string languageTag; // BCP 47 ("en-US")
|
||||
std::vector<KeyDescriptor> keys; // all keys
|
||||
float layoutWidth; // total keyboard width in dp
|
||||
float layoutHeight; // total keyboard height in dp
|
||||
|
||||
int32_t findNearestKey(float x, float y) const; // nearest char key index
|
||||
int32_t findKeyByCodePoint(int32_t codePoint) const; // by code point (case-insensitive)
|
||||
bool isValid() const; // ≥ 1 character key
|
||||
};
|
||||
```
|
||||
|
||||
**Coordinate system:** Origin is the top-left corner of the keyboard. All values are in density-independent pixels (dp). The same dp coordinates must be used for both the layout and the gesture touch points.
|
||||
|
||||
---
|
||||
|
||||
### ScoringConfig
|
||||
|
||||
**Header:** `SwipeTypeTypes.h`
|
||||
|
||||
```cpp
|
||||
struct ScoringConfig {
|
||||
int resampleCount = 64; // points after resampling
|
||||
float minPointDistance = 2.0f; // dedup threshold (dp)
|
||||
float dtwBandwidthRatio = 0.10f; // Sakoe-Chiba band = ceil(0.10 * 64) = 6
|
||||
float frequencyWeight = 0.30f; // α: weight of frequency in final score
|
||||
int maxCandidatesEvaluated = 20; // hard cap on evaluated candidates
|
||||
float lengthFilterTolerance = 3.0f; // ± tolerance for word-length filter
|
||||
float maxDTWFloor = 3.0f; // absolute floor for DTW normalization
|
||||
};
|
||||
```
|
||||
|
||||
Pass a modified config to `GestureEngine::configure()` to tune scoring behavior.
|
||||
|
||||
---
|
||||
|
||||
### DictionaryLoader
|
||||
|
||||
**Header:** `DictionaryLoader.h`
|
||||
|
||||
```cpp
|
||||
struct DictionaryEntry {
|
||||
std::string word; // UTF-8 word
|
||||
uint32_t frequency; // higher = more common
|
||||
uint8_t flags; // DICT_FLAG_PROPER_NOUN, DICT_FLAG_PROFANITY
|
||||
};
|
||||
|
||||
class DictionaryLoader {
|
||||
public:
|
||||
bool load(const std::string& filePath);
|
||||
bool loadFromMemory(const uint8_t* data, size_t size);
|
||||
void unload();
|
||||
|
||||
const std::vector<DictionaryEntry>& getAllEntries() const;
|
||||
std::vector<const DictionaryEntry*> getEntriesStartingWith(char c) const;
|
||||
std::vector<const DictionaryEntry*> getEntriesWithStartEnd(char start, char end) const;
|
||||
uint32_t getMaxFrequency() const;
|
||||
ErrorInfo getLastError() const;
|
||||
};
|
||||
```
|
||||
|
||||
The loader reads the binary `.glide` format (see `scripts/gen_dict.py`).
|
||||
**Thread safety:** Read-only operations are safe after loading. Load/unload are not thread-safe.
|
||||
|
||||
---
|
||||
|
||||
### Error Handling
|
||||
|
||||
```cpp
|
||||
enum class ErrorCode : int {
|
||||
NONE = 0,
|
||||
DICT_NOT_FOUND = 1,
|
||||
DICT_CORRUPT = 2,
|
||||
DICT_VERSION_MISMATCH = 3,
|
||||
LAYOUT_INVALID = 4,
|
||||
PATH_TOO_SHORT = 5,
|
||||
ENGINE_NOT_INITIALIZED = 6,
|
||||
OUT_OF_MEMORY = 7
|
||||
};
|
||||
|
||||
struct ErrorInfo {
|
||||
ErrorCode code;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
using ErrorCallback = std::function<void(const ErrorInfo& error)>;
|
||||
```
|
||||
|
||||
Errors are reported via:
|
||||
1. Return values (`false` from `init()`, empty vector from `recognize()`)
|
||||
2. `getLastError()` — last error info
|
||||
3. `setErrorCallback()` — synchronous callback on error
|
||||
|
||||
---
|
||||
|
||||
### Constants
|
||||
|
||||
| Constant | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| `RESAMPLE_COUNT` | `64` | Points after resampling |
|
||||
| `MIN_POINT_DISTANCE_DP` | `2.0f` | Dedup threshold (dp) |
|
||||
| `MIN_GESTURE_POINTS` | `2` | Minimum points for a valid gesture |
|
||||
| `MAX_GESTURE_POINTS` | `10000` | Hard cap on raw input points |
|
||||
| `DTW_BANDWIDTH` | `6` | Sakoe-Chiba band width |
|
||||
| `FREQUENCY_WEIGHT` | `0.30f` | Default α for frequency weighting |
|
||||
| `LENGTH_FILTER_TOLERANCE` | `3.0f` | Word-length filter tolerance (±) |
|
||||
| `MAX_DTW_FLOOR` | `3.0f` | Absolute DTW normalization floor |
|
||||
| `DEFAULT_MAX_CANDIDATES` | `8` | Default max candidates |
|
||||
| `MAX_MAX_CANDIDATES` | `20` | Hard cap on max candidates |
|
||||
| `DICT_MAGIC` | `0x474C4944` | `.glide` file magic ("GLID") |
|
||||
| `DICT_VERSION` | `1` | Current dict format version |
|
||||
| `DICT_HEADER_SIZE` | `32` | Fixed header size in bytes |
|
||||
| `MAX_WORD_LENGTH` | `64` | Max word length (UTF-8 bytes) |
|
||||
|
||||
---
|
||||
|
||||
## Android (Java) API
|
||||
|
||||
Package: `dev.dettmer.swipetype.android`
|
||||
|
||||
### SwipeTypeEngine
|
||||
|
||||
The main Android entry point. Wraps the C++ core via JNI.
|
||||
|
||||
```java
|
||||
public class SwipeTypeEngine {
|
||||
// Lifecycle
|
||||
void init(Context context, SwipeTypeAdapter adapter);
|
||||
boolean loadDictionary(String languageTag, InputStream dictStream);
|
||||
void shutdown();
|
||||
boolean isInitialized();
|
||||
|
||||
// Recognition
|
||||
void processGesture(List<GesturePoint> points);
|
||||
|
||||
// Layout
|
||||
void notifyLayoutChanged();
|
||||
}
|
||||
```
|
||||
|
||||
#### `init(context, adapter)`
|
||||
|
||||
Store the app context and adapter. Does NOT load the dictionary or initialize native code. Call `loadDictionary()` after this.
|
||||
|
||||
#### `loadDictionary(languageTag, dictStream) → boolean`
|
||||
|
||||
Copy the dictionary to the cache directory, query the adapter for the current layout, and initialize the native engine. Calls `adapter.onInit(this)` on success.
|
||||
|
||||
#### `processGesture(points)`
|
||||
|
||||
Run recognition on the given touch points. Results are delivered synchronously via `adapter.onCandidatesReady()` on the calling thread.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `points` | `List<GesturePoint>` | ≥ 2 touch points with dp coordinates |
|
||||
|
||||
#### `notifyLayoutChanged()`
|
||||
|
||||
Re-query the adapter for the current layout and update the native engine. Call after device rotation, language switch, or layout resize.
|
||||
|
||||
#### `shutdown()`
|
||||
|
||||
Release all native resources. Safe to call multiple times.
|
||||
|
||||
**Thread safety:** All public methods are `synchronized`. `processGesture()` can be called from any thread.
|
||||
|
||||
---
|
||||
|
||||
### SwipeTypeAdapter
|
||||
|
||||
The contract between libswipetype and any keyboard app. Every keyboard integration must implement this interface.
|
||||
|
||||
```java
|
||||
public interface SwipeTypeAdapter {
|
||||
void onInit(SwipeTypeEngine engine);
|
||||
KeyboardLayoutDescriptor getKeyboardLayout();
|
||||
void onCandidatesReady(List<SwipeTypeCandidate> candidates);
|
||||
void onError(SwipeTypeError error);
|
||||
}
|
||||
```
|
||||
|
||||
| Method | When called | What to do |
|
||||
|--------|-------------|------------|
|
||||
| `onInit` | After `loadDictionary()` succeeds | Store engine reference if needed |
|
||||
| `getKeyboardLayout` | During init and `notifyLayoutChanged()` | Return current key positions in dp |
|
||||
| `onCandidatesReady` | After `processGesture()` | Show candidates in suggestion bar |
|
||||
| `onError` | On any error | Log and optionally show user message |
|
||||
|
||||
---
|
||||
|
||||
### GesturePoint (Java)
|
||||
|
||||
```java
|
||||
public class GesturePoint {
|
||||
public final float x; // dp
|
||||
public final float y; // dp
|
||||
public final long timestamp; // ms since gesture start
|
||||
|
||||
public GesturePoint(float x, float y, long timestamp);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### KeyboardLayoutDescriptor
|
||||
|
||||
```java
|
||||
public class KeyboardLayoutDescriptor {
|
||||
public final String languageTag;
|
||||
public final List<KeyInfo> keys;
|
||||
public final float layoutWidth; // dp
|
||||
public final float layoutHeight; // dp
|
||||
|
||||
public static class KeyInfo {
|
||||
public final String label;
|
||||
public final int codePoint; // Unicode. -1 for non-char keys
|
||||
public final float centerX; // dp
|
||||
public final float centerY; // dp
|
||||
public final float width; // dp
|
||||
public final float height; // dp
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SwipeTypeCandidate (Java)
|
||||
|
||||
```java
|
||||
public final class SwipeTypeCandidate {
|
||||
public final String word; // recognized word
|
||||
public final float confidence; // [0.0, 1.0]
|
||||
public final int sourceFlags; // bitmask
|
||||
|
||||
public static final int SOURCE_MAIN_DICT = 0x01;
|
||||
public static final int SOURCE_USER_DICT = 0x02;
|
||||
public static final int SOURCE_COMPLETION = 0x04;
|
||||
|
||||
public boolean isFromMainDict();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SwipeTypeError
|
||||
|
||||
```java
|
||||
public class SwipeTypeError {
|
||||
public final int code;
|
||||
public final String message;
|
||||
|
||||
// Predefined error constants:
|
||||
public static final SwipeTypeError DICT_NOT_FOUND;
|
||||
public static final SwipeTypeError DICT_CORRUPT;
|
||||
public static final SwipeTypeError LAYOUT_INVALID;
|
||||
public static final SwipeTypeError PATH_TOO_SHORT;
|
||||
public static final SwipeTypeError ENGINE_NOT_INITIALIZED;
|
||||
public static final SwipeTypeError JNI_ERROR;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,357 @@
|
||||
# Architecture — libswipetype
|
||||
|
||||
> **Version:** 0.1.0 | **Updated:** 2026-02
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
libswipetype is a swipe/glide typing engine that converts finger gestures into word candidates. The system has three layers:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Keyboard App (HeliBoard, FlorisBoard, sample-app, etc.) │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│ SwipeTypeAdapter interface
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ swipetype-android (Java + JNI) │
|
||||
│ SwipeTypeEngine → GestureLibJNI.cpp → native calls │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│ C ABI (JNI function pointers)
|
||||
┌───────────────────────────▼─────────────────────────────────┐
|
||||
│ swipetype-core (C++17, zero dependencies) │
|
||||
│ GestureEngine → PathProcessor → IdealPathGenerator → │
|
||||
│ Scorer → DictionaryLoader │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Map
|
||||
|
||||
| Module | Language | Artifact | Purpose |
|
||||
|--------|----------|----------|---------|
|
||||
| `swipetype-core/` | C++17 | `libswipetype-core.a` | Pure algorithm library. Zero external dependencies. |
|
||||
| `swipetype-android/` | Java + JNI | `swipetype-android.aar` | Android AAR wrapping the core via JNI |
|
||||
| `adapters/heliboard/` | Java | Source files | Reference adapter for the HeliBoard keyboard |
|
||||
| `sample-app/` | Java | Debug APK | Minimal IME demonstrating the full integration |
|
||||
| `scripts/` | Python | CLI tools | Dictionary generation (`gen_dict.py`) |
|
||||
| `test-data/` | JSON, TSV | Test fixtures | Keyboard layouts, gesture scenarios, word lists |
|
||||
|
||||
---
|
||||
|
||||
## Core Recognition Pipeline
|
||||
|
||||
The recognition pipeline runs inside `GestureEngine::recognize()`. All steps execute synchronously on the calling thread.
|
||||
|
||||
```
|
||||
Raw touch points (dp)
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 1. PathProcessor │ deduplicate → resample(64) → normalize([0,1])
|
||||
│ normalize() │
|
||||
└───────┬───────────┘
|
||||
│ GesturePath (64 NormalizedPoints)
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 2. Start/End Key │ Find nearest key to first & last touch point
|
||||
│ Detection │
|
||||
└───────┬───────────┘
|
||||
│ startChar, endChar
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 3. Candidate │ Filter dictionary by start+end char, then by
|
||||
│ Filtering │ estimated word length (key-transition count ±3)
|
||||
└───────┬───────────┘
|
||||
│ filtered DictionaryEntry list
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 4. Ideal Path │ For each candidate word, generate the "ideal"
|
||||
│ Generation │ swipe path through key centers. Cached per word.
|
||||
│ (IPG) │
|
||||
└───────┬───────────┘
|
||||
│ GesturePath per candidate
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 5. DTW Scoring │ Compute DTW distance between gesture and each
|
||||
│ (Scorer) │ ideal path using Sakoe-Chiba band (W=6)
|
||||
└───────┬───────────┘
|
||||
│ (word, dtwDistance) pairs
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 6. Confidence │ Normalize DTW, apply frequency weight (adaptive α),
|
||||
│ Computation │ compute confidence = 1 - finalScore
|
||||
└───────┬───────────┘
|
||||
│ GestureCandidate list
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 7. Sort & Prune │ Sort by confidence descending, truncate to maxCandidates
|
||||
└───────┬───────────┘
|
||||
│
|
||||
▼
|
||||
vector<GestureCandidate>
|
||||
```
|
||||
|
||||
### Step 1: Path Normalization (PathProcessor)
|
||||
|
||||
**File:** `swipetype-core/src/PathProcessor.cpp`
|
||||
|
||||
Three sub-steps:
|
||||
|
||||
1. **Deduplicate** — Remove consecutive points closer than `MIN_POINT_DISTANCE_DP` (2.0 dp). Always keeps first and last point.
|
||||
2. **Resample** — Equidistant resampling to exactly `RESAMPLE_COUNT` (64) points along the path arc. Uses the $1 Unistroke algorithm (Wobbrock et al., 2007).
|
||||
3. **Bounding-box normalize** — Scale coordinates to [0.0, 1.0] preserving aspect ratio. Normalizes time to [0.0, 1.0].
|
||||
|
||||
Output: `GesturePath` with 64 `NormalizedPoint`s plus metadata (arc length, start/end key indices, aspect ratio).
|
||||
|
||||
### Step 2: Start/End Key Detection
|
||||
|
||||
Uses `KeyboardLayout::findNearestKey()` on the first and last raw touch points (not the resampled points). Maps to lowercase ASCII characters for dictionary lookup.
|
||||
|
||||
### Step 3: Candidate Filtering
|
||||
|
||||
Three-tier filter cascade:
|
||||
|
||||
1. `getEntriesWithStartEnd(startChar, endChar)` — words matching both start and end character
|
||||
2. `getEntriesStartingWith(startChar)` — fallback if tier 1 yields nothing
|
||||
3. `getAllEntries()` — last resort brute-force
|
||||
|
||||
After tier selection, a **word-length filter** eliminates candidates whose character count differs from the estimated word length by more than `LENGTH_FILTER_TOLERANCE` (±3.0). The estimate uses **key-transition counting**: walk the raw gesture path, snap each point to its nearest key, count distinct key transitions.
|
||||
|
||||
### Step 4: Ideal Path Generation (IdealPathGenerator)
|
||||
|
||||
**File:** `swipetype-core/src/IdealPathGenerator.cpp`
|
||||
|
||||
For each dictionary word, generates the "perfect" swipe path by connecting key centers with straight lines, then resampling to 64 points. Duplicate consecutive keys (e.g., "l" in "hello") are collapsed to a single key center.
|
||||
|
||||
Results are **cached** per word (invalidated when the layout changes via `setLayout()`).
|
||||
|
||||
### Step 5: DTW Scoring (Scorer)
|
||||
|
||||
**File:** `swipetype-core/src/Scorer.cpp`
|
||||
|
||||
Computes Dynamic Time Warping (DTW) distance between the gesture path and each ideal path. Uses:
|
||||
|
||||
- **Sakoe-Chiba band** with width `W = ceil(0.10 × 64) = 6` to constrain the warping window
|
||||
- **Two-row rolling array** for O(N × W) time and O(N) space
|
||||
- **Euclidean distance** between NormalizedPoint(x, y) pairs as the local cost function
|
||||
- Final DTW divided by path length (N=64) for per-point normalization
|
||||
|
||||
### Step 6: Confidence Computation
|
||||
|
||||
```
|
||||
maxDTW = max(maxCandidateDTW, MAX_DTW_FLOOR) // floor only for single-candidate; multi uses raw maxDTW
|
||||
normalizedDTW = min(1.0, dtwDistance / maxDTW)
|
||||
normalizedFreq = frequency / maxFrequency
|
||||
|
||||
// Adaptive alpha: proportional scaling based on DTW range
|
||||
effectiveAlpha = α × max(0.1, rawRange / 0.5)
|
||||
|
||||
finalScore = (1 - effectiveAlpha) × normalizedDTW + effectiveAlpha × (1 - normalizedFreq)
|
||||
confidence = 1.0 - clamp(finalScore, 0, 1)
|
||||
```
|
||||
|
||||
### Step 7: Sort & Prune
|
||||
|
||||
Sort by confidence descending. Truncate to `maxCandidates` (default 8, max 20).
|
||||
|
||||
---
|
||||
|
||||
## Android Integration Layer
|
||||
|
||||
### JNI Bridge
|
||||
|
||||
**File:** `swipetype-android/src/main/cpp/GestureLibJNI.cpp`
|
||||
|
||||
Translates Java arrays into C++ types and vice versa. The JNI layer is thin — it only marshals data and forwards to `GestureEngine`.
|
||||
|
||||
| JNI Function | Calls |
|
||||
|-------------|-------|
|
||||
| `nativeInit()` | `GestureEngine::init()` |
|
||||
| `nativeInitWithData()` | `GestureEngine::initWithData()` |
|
||||
| `nativeRecognize()` | `GestureEngine::recognize()` |
|
||||
| `nativeUpdateLayout()` | `GestureEngine::updateLayout()` |
|
||||
| `nativeShutdown()` | `GestureEngine::shutdown()` |
|
||||
|
||||
The native library is named `glide_jni` and loaded via `System.loadLibrary("glide_jni")`.
|
||||
|
||||
### SwipeTypeEngine (Java)
|
||||
|
||||
**File:** `swipetype-android/src/main/java/dev/dettmer/swipetype/android/SwipeTypeEngine.java`
|
||||
|
||||
Manages the lifecycle:
|
||||
1. `init(context, adapter)` — stores context and adapter reference
|
||||
2. `loadDictionary(tag, stream)` — copies stream to cache, queries adapter for layout, calls `nativeInit()`
|
||||
3. `processGesture(points)` — converts `List<GesturePoint>` to arrays, calls `nativeRecognize()`, wraps results in `SwipeTypeCandidate` list, delivers via `adapter.onCandidatesReady()`
|
||||
4. `notifyLayoutChanged()` — re-queries layout and calls `nativeUpdateLayout()`
|
||||
5. `shutdown()` — calls `nativeShutdown()`
|
||||
|
||||
All public methods are `synchronized`.
|
||||
|
||||
---
|
||||
|
||||
## Dictionary Format
|
||||
|
||||
Binary `.glide` format produced by `scripts/gen_dict.py` from a TSV word list.
|
||||
|
||||
### File Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Header (32 bytes, fixed) │
|
||||
├──────────────────────────────┤
|
||||
│ Entry 0 │
|
||||
│ Entry 1 │
|
||||
│ ... │
|
||||
│ Entry N-1 │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
### Header (32 bytes)
|
||||
|
||||
| Offset | Size | Field | Value |
|
||||
|--------|------|-------|-------|
|
||||
| 0 | 4 | magic | `0x474C4944` ("GLID", little-endian) |
|
||||
| 4 | 2 | version | `1` |
|
||||
| 6 | 2 | flags | `0` (reserved) |
|
||||
| 8 | 4 | entryCount | number of words |
|
||||
| 12 | 2 | langLen | length of language tag |
|
||||
| 14 | N | langTag | UTF-8 language tag (e.g., "en") |
|
||||
| 14+N | padding | — | zeros to byte 32 |
|
||||
|
||||
### Entry Format
|
||||
|
||||
| Size | Field | Description |
|
||||
|------|-------|-------------|
|
||||
| 1 | wordLen | Length of word in bytes |
|
||||
| wordLen | word | UTF-8 word string |
|
||||
| 4 | frequency | Little-endian uint32 |
|
||||
| 1 | flags | `DICT_FLAG_PROPER_NOUN` (0x01), `DICT_FLAG_PROFANITY` (0x02) |
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
libswipetype/
|
||||
├── swipetype-core/ # C++17 core library
|
||||
│ ├── CMakeLists.txt # Build config (standalone or via Gradle)
|
||||
│ ├── include/swipetype/ # Public headers
|
||||
│ │ ├── GestureEngine.h # Main API
|
||||
│ │ ├── GesturePath.h # Path data structures
|
||||
│ │ ├── GesturePoint.h # Point types (raw + normalized)
|
||||
│ │ ├── GestureCandidate.h # Recognition result
|
||||
│ │ ├── KeyboardLayout.h # Layout descriptor
|
||||
│ │ ├── PathProcessor.h # Path normalization
|
||||
│ │ ├── IdealPathGenerator.h # Reference path generation
|
||||
│ │ ├── Scorer.h # DTW scoring
|
||||
│ │ ├── DictionaryLoader.h # Dictionary I/O
|
||||
│ │ └── SwipeTypeTypes.h # Shared types / constants
|
||||
│ ├── src/ # Implementation files
|
||||
│ │ ├── GestureEngine.cpp
|
||||
│ │ ├── PathProcessor.cpp
|
||||
│ │ ├── IdealPathGenerator.cpp
|
||||
│ │ ├── Scorer.cpp
|
||||
│ │ ├── DictionaryLoader.cpp
|
||||
│ │ └── AdjacencyMap.cpp
|
||||
│ └── tests/ # Google Test suite
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── TestHelpers.h
|
||||
│ ├── PathProcessorTest.cpp
|
||||
│ ├── ScorerTest.cpp
|
||||
│ ├── DictionaryLoaderTest.cpp
|
||||
│ ├── GestureEngineTest.cpp
|
||||
│ └── IdealPathGeneratorTest.cpp
|
||||
├── swipetype-android/ # Android AAR module
|
||||
│ ├── build.gradle # Gradle + CMake NDK build
|
||||
│ └── src/main/
|
||||
│ ├── cpp/GestureLibJNI.cpp # JNI bridge
|
||||
│ └── java/dev/dettmer/swipetype/android/
|
||||
│ ├── SwipeTypeEngine.java
|
||||
│ ├── SwipeTypeAdapter.java
|
||||
│ ├── GesturePoint.java
|
||||
│ ├── KeyboardLayoutDescriptor.java
|
||||
│ ├── SwipeTypeCandidate.java
|
||||
│ └── SwipeTypeError.java
|
||||
├── adapters/
|
||||
│ └── heliboard/ # Reference adapter for HeliBoard
|
||||
│ └── src/main/java/dev/dettmer/swipetype/adapters/heliboard/
|
||||
│ └── HeliboardSwipeTypeAdapter.java
|
||||
├── sample-app/ # Minimal sample IME
|
||||
│ ├── build.gradle
|
||||
│ └── src/main/java/dev/dettmer/swipetype/sample/
|
||||
│ ├── MainActivity.java
|
||||
│ ├── SampleInputMethodService.java
|
||||
│ └── SampleKeyboardView.java
|
||||
├── scripts/
|
||||
│ └── gen_dict.py # TSV → .glide dictionary generator
|
||||
├── test-data/
|
||||
│ ├── en-us-full.tsv # 302-word English word list
|
||||
│ ├── gesture-scenarios.json # Test gesture definitions
|
||||
│ └── qwerty-standard.json # Standard QWERTY layout definition
|
||||
├── docs/ # Documentation (this directory)
|
||||
│ ├── API.md
|
||||
│ ├── ARCHITECTURE.md (this file)
|
||||
│ ├── ONBOARDING.md
|
||||
│ └── HOW_TO_WRITE_AN_ADAPTER.md
|
||||
├── CHANGELOG.md
|
||||
├── CONTRIBUTING.md
|
||||
├── LICENSE (Apache 2.0)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Why pImpl?
|
||||
|
||||
All core classes (`GestureEngine`, `PathProcessor`, `IdealPathGenerator`, `Scorer`, `DictionaryLoader`) use the pointer-to-implementation (pImpl) idiom. This:
|
||||
|
||||
1. **Hides implementation details** from public headers — keyboard apps only include headers, never see internal types
|
||||
2. **Enables binary compatibility** — internal changes don't break ABI
|
||||
3. **Reduces compile times** — changes to `.cpp` files don't force recompilation of dependents
|
||||
|
||||
### Why DTW instead of neural networks?
|
||||
|
||||
DTW is deterministic, explainable, and requires zero training data. It runs in under 5ms per candidate on mid-range Android devices. The tradeoff is lower accuracy on edge cases (very short words, similar shapes), which is acceptable for a v0.1 library.
|
||||
|
||||
### Why bounding-box normalization?
|
||||
|
||||
Normalizing gesture and ideal paths to a [0, 1] bounding box makes the DTW comparison scale-invariant. A gesture on a tablet (high dp) produces the same normalized path as one on a phone (low dp). Aspect ratio is preserved to disambiguate horizontal vs. vertical swipes.
|
||||
|
||||
### Why key-transition counting for length estimation?
|
||||
|
||||
The original arc-length heuristic divided total gesture arc length by average key spacing. This overestimated drastically for zigzag words (e.g., "hello" estimated at 17+ characters). Key-transition counting walks the raw path, snaps each point to its nearest key, and counts distinct transitions — yielding an estimate that closely matches actual word length regardless of path geometry.
|
||||
|
||||
### Why adaptive frequency weight?
|
||||
|
||||
A fixed frequency weight (α = 0.30) allows high-frequency words to dominate when DTW scores are compressed (all candidates match similarly well). The adaptive approach scales α proportionally with the DTW range (`effectiveAlpha *= max(0.1, rawRange/0.5)`), so when scores are tightly clustered, frequency influence shrinks smoothly. This ensures shape remains the primary discriminator unless there's a clear shape winner.
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Operation | Time | Notes |
|
||||
|-----------|------|-------|
|
||||
| `normalize()` | < 1ms | Resample 64 points |
|
||||
| `getIdealPath()` (cached) | < 10μs | Hash map lookup |
|
||||
| `getIdealPath()` (miss) | < 0.5ms | Generate + resample |
|
||||
| `computeDTWDistance()` | < 2ms | 64×64 with band W=6 |
|
||||
| `recognize()` (full pipeline) | < 50ms | With 302-word dictionary |
|
||||
|
||||
Memory: ~10KB per loaded dictionary word (entry + cached ideal path). 302 words ≈ 3MB.
|
||||
|
||||
---
|
||||
|
||||
## Thread Safety
|
||||
|
||||
| Component | Thread Safety |
|
||||
|-----------|---------------|
|
||||
| `GestureEngine` (C++) | NOT thread-safe. External sync required |
|
||||
| `SwipeTypeEngine` (Java) | All public methods `synchronized` |
|
||||
| `DictionaryLoader` (after load) | Read-only operations thread-safe |
|
||||
| `PathProcessor` | Stateless after construction — thread-safe |
|
||||
| `Scorer` | Stateless after `configure()` — thread-safe |
|
||||
| `IdealPathGenerator` | NOT thread-safe (mutable cache) |
|
||||
@@ -0,0 +1,358 @@
|
||||
# How to Write an Adapter — libswipetype
|
||||
|
||||
> A step-by-step guide to integrating libswipetype into any Android keyboard app.
|
||||
|
||||
---
|
||||
|
||||
## What is an Adapter?
|
||||
|
||||
An adapter is the glue between a keyboard app and libswipetype. It implements the `SwipeTypeAdapter` interface, translating between the keyboard app's internal types and the generic swipetype API.
|
||||
|
||||
```
|
||||
┌────────────────────┐ ┌──────────────────────┐
|
||||
│ Your Keyboard App │◄──────►│ YourSwipeTypeAdapter │
|
||||
│ (e.g., FlorisBoard)│ │ implements │
|
||||
│ Key positions │ │ SwipeTypeAdapter │
|
||||
│ Touch events │ │ │
|
||||
│ Suggestion bar │ │ ┌──────────────────┐ │
|
||||
└────────────────────┘ │ │ SwipeTypeEngine │ │
|
||||
│ │ (libswipetype) │ │
|
||||
│ └──────────────────┘ │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Your keyboard app can provide key positions (center, width, height) in dp
|
||||
- You have access to touch events (x, y, timestamp) during a swipe gesture
|
||||
- You have a place to display word candidates (suggestion bar, popup, etc.)
|
||||
- The `swipetype-android` AAR is available as a dependency
|
||||
|
||||
### Adding the dependency
|
||||
|
||||
In your keyboard module's `build.gradle`:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation project(':swipetype-android')
|
||||
// Or, when published:
|
||||
// implementation 'dev.dettmer.swipetype:swipetype-android:0.1.0'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Guide
|
||||
|
||||
### Step 1: Implement `SwipeTypeAdapter`
|
||||
|
||||
Create a class that implements all four methods:
|
||||
|
||||
```java
|
||||
package com.example.mykeyboard;
|
||||
|
||||
import dev.dettmer.swipetype.android.*;
|
||||
import java.util.List;
|
||||
|
||||
public class MySwipeTypeAdapter implements SwipeTypeAdapter {
|
||||
|
||||
private final MyKeyboardService service;
|
||||
private SwipeTypeEngine engine;
|
||||
|
||||
public MySwipeTypeAdapter(MyKeyboardService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
// ── Called after loadDictionary() succeeds ───────────────────────
|
||||
@Override
|
||||
public void onInit(SwipeTypeEngine engine) {
|
||||
this.engine = engine;
|
||||
// Store the engine reference for later use (e.g., shutdown)
|
||||
// Do NOT call loadDictionary() from here — it's already loaded.
|
||||
}
|
||||
|
||||
// ── Called by the engine to get key positions ────────────────────
|
||||
@Override
|
||||
public KeyboardLayoutDescriptor getKeyboardLayout() {
|
||||
// Translate your keyboard's internal layout to KeyboardLayoutDescriptor
|
||||
List<KeyboardLayoutDescriptor.KeyInfo> keys = new ArrayList<>();
|
||||
|
||||
for (MyKey key : service.getCurrentKeys()) {
|
||||
// IMPORTANT: All coordinates must be in dp, not pixels!
|
||||
keys.add(new KeyboardLayoutDescriptor.KeyInfo(
|
||||
key.getLabel(), // "a", "b", etc.
|
||||
key.getCodePoint(), // Unicode code point (97 for 'a')
|
||||
key.getCenterXDp(), // center X in dp
|
||||
key.getCenterYDp(), // center Y in dp
|
||||
key.getWidthDp(), // key width in dp
|
||||
key.getHeightDp() // key height in dp
|
||||
));
|
||||
}
|
||||
|
||||
return new KeyboardLayoutDescriptor(
|
||||
"en-US", // BCP 47 language tag
|
||||
keys,
|
||||
service.getKeyboardWidthDp(), // total keyboard width
|
||||
service.getKeyboardHeightDp() // total keyboard height
|
||||
);
|
||||
}
|
||||
|
||||
// ── Called with recognition results ──────────────────────────────
|
||||
@Override
|
||||
public void onCandidatesReady(List<SwipeTypeCandidate> candidates) {
|
||||
// Forward to your keyboard's suggestion bar
|
||||
List<String> words = new ArrayList<>();
|
||||
for (SwipeTypeCandidate c : candidates) {
|
||||
words.add(c.word); // public field, not a getter
|
||||
}
|
||||
service.showSuggestions(words);
|
||||
}
|
||||
|
||||
// ── Called on errors ─────────────────────────────────────────────
|
||||
@Override
|
||||
public void onError(SwipeTypeError error) {
|
||||
Log.e("MyAdapter", "SwipeType error [" + error.code + "]: " + error.message);
|
||||
// Optionally show a user-facing message for critical errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Initialize the Engine
|
||||
|
||||
In your keyboard service's `onCreate()`:
|
||||
|
||||
```java
|
||||
public class MyKeyboardService extends InputMethodService {
|
||||
|
||||
private SwipeTypeEngine engine;
|
||||
private MySwipeTypeAdapter adapter;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
adapter = new MySwipeTypeAdapter(this);
|
||||
engine = new SwipeTypeEngine();
|
||||
engine.init(this, adapter);
|
||||
|
||||
// Load dictionary — onInit() callback fires on success
|
||||
loadDictionary();
|
||||
}
|
||||
|
||||
private void loadDictionary() {
|
||||
// Option A: From APK raw resources
|
||||
int resId = getResources().getIdentifier("en_us", "raw", getPackageName());
|
||||
if (resId != 0) {
|
||||
try (InputStream is = getResources().openRawResource(resId)) {
|
||||
engine.loadDictionary("en-US", is);
|
||||
} catch (Exception e) {
|
||||
Log.e("MyIME", "Dict load failed: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Option B: From a file on disk
|
||||
// try (InputStream is = new FileInputStream("/path/to/dict.glide")) {
|
||||
// engine.loadDictionary("en-US", is);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (engine != null) engine.shutdown();
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Feed Touch Events
|
||||
|
||||
Collect `ACTION_DOWN` → `ACTION_MOVE` → `ACTION_UP` touch events during a swipe gesture and convert them to `GesturePoint` objects:
|
||||
|
||||
```java
|
||||
// In your keyboard view's onTouchEvent():
|
||||
private List<GesturePoint> activePath = new ArrayList<>();
|
||||
private long gestureStartMs;
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// Convert pixels to dp!
|
||||
float xDp = event.getX() / getResources().getDisplayMetrics().density;
|
||||
float yDp = event.getY() / getResources().getDisplayMetrics().density;
|
||||
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
activePath.clear();
|
||||
gestureStartMs = event.getEventTime();
|
||||
activePath.add(new GesturePoint(xDp, yDp, 0));
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
long elapsed = event.getEventTime() - gestureStartMs;
|
||||
activePath.add(new GesturePoint(xDp, yDp, elapsed));
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (activePath.size() >= 2) {
|
||||
// This triggers recognition → onCandidatesReady()
|
||||
engine.notifyLayoutChanged(); // ensure layout is fresh
|
||||
engine.processGesture(new ArrayList<>(activePath));
|
||||
}
|
||||
activePath.clear();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Handle Layout Changes
|
||||
|
||||
Call `engine.notifyLayoutChanged()` when:
|
||||
|
||||
- The user rotates the device
|
||||
- The user switches languages
|
||||
- The keyboard layout changes size
|
||||
- Before the first `processGesture()` call (if layout may have changed since init)
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void onStartInputView(EditorInfo info, boolean restarting) {
|
||||
super.onStartInputView(info, restarting);
|
||||
if (engine != null && engine.isInitialized()) {
|
||||
engine.notifyLayoutChanged();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Coordinate Conversion Checklist
|
||||
|
||||
The **#1 cause of wrong results** is coordinate mismatch. Use this checklist:
|
||||
|
||||
- [ ] Key positions are in **dp** (not pixels)
|
||||
- [ ] Touch event coordinates are converted from pixels to **dp**
|
||||
- [ ] Key `centerX`/`centerY` are relative to the **keyboard view's top-left** (not the screen)
|
||||
- [ ] `layoutWidth`/`layoutHeight` match the keyboard view's actual **dp dimensions**
|
||||
- [ ] Both the layout and touch events use the **same coordinate space**
|
||||
|
||||
Quick dp conversion:
|
||||
```java
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
float dp = pixels / density;
|
||||
float pixels = dp * density;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference: HeliBoard Adapter
|
||||
|
||||
The HeliBoard adapter in `adapters/heliboard/` is a working reference implementation.
|
||||
|
||||
Key points:
|
||||
- Translates HeliBoard's `ProximityInfo` and `Key[]` objects into `KeyboardLayoutDescriptor`
|
||||
- Translates HeliBoard's `InputPointers` into `List<GesturePoint>`
|
||||
- Delivers candidates to HeliBoard's `SuggestedWords` interface
|
||||
|
||||
File: `adapters/heliboard/src/main/java/dev/dettmer/swipetype/adapters/heliboard/HeliboardSwipeTypeAdapter.java`
|
||||
|
||||
---
|
||||
|
||||
## Generating a Dictionary
|
||||
|
||||
The engine requires a binary `.glide` dictionary file. Generate one from a TSV word list:
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_dict.py \
|
||||
--input words.tsv \
|
||||
--output mydict.glide \
|
||||
--language en-US
|
||||
```
|
||||
|
||||
TSV format (no header):
|
||||
```
|
||||
the 1000000
|
||||
and 800000
|
||||
hello 50000
|
||||
world 40000
|
||||
```
|
||||
|
||||
Tab-separated: `word<TAB>frequency`. Higher frequency = more likely to be suggested when DTW scores are similar.
|
||||
|
||||
Place the `.glide` file in your APK's `res/raw/` directory with underscores instead of hyphens (e.g., `en_us.glide` → accessed as `R.raw.en_us`).
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
| Pitfall | Symptom | Fix |
|
||||
|---------|---------|-----|
|
||||
| Coordinates in pixels instead of dp | Empty results or wrong candidates | Divide by `displayMetrics.density` |
|
||||
| Missing `loadDictionary()` call | `onCandidatesReady()` never fires | Call `loadDictionary()` in `onCreate()` |
|
||||
| Calling `loadDictionary()` from `onInit()` | Infinite loop / stack overflow | `onInit` is a callback — don't reload from it |
|
||||
| Layout not updated before gesture | First gesture returns wrong results | Call `notifyLayoutChanged()` before `processGesture()` |
|
||||
| Using `getWord()` instead of `.word` | Compile error | `SwipeTypeCandidate` uses public fields, not getters |
|
||||
| Wrong native lib name | `UnsatisfiedLinkError` | The lib is `glide_jni`, ensure it's in `jniLibs/` |
|
||||
| Dictionary too small | Poor accuracy | Use ≥ 10,000 words for production quality |
|
||||
|
||||
---
|
||||
|
||||
## Minimal Working Example
|
||||
|
||||
A complete, self-contained adapter in ~60 lines:
|
||||
|
||||
```java
|
||||
public class MinimalAdapter implements SwipeTypeAdapter {
|
||||
private final InputMethodService ime;
|
||||
|
||||
public MinimalAdapter(InputMethodService ime) { this.ime = ime; }
|
||||
|
||||
@Override
|
||||
public void onInit(SwipeTypeEngine engine) {
|
||||
Log.i("Minimal", "Engine ready");
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyboardLayoutDescriptor getKeyboardLayout() {
|
||||
// Hardcoded QWERTY layout (360×260 dp)
|
||||
float w = 360f, h = 260f, kw = 36f, kh = 65f;
|
||||
String[] rows = {"qwertyuiop", "asdfghjkl", "zxcvbnm"};
|
||||
List<KeyboardLayoutDescriptor.KeyInfo> keys = new ArrayList<>();
|
||||
float rowY = kh / 2f;
|
||||
for (String row : rows) {
|
||||
float offsetX = (w - row.length() * kw) / 2f + kw / 2f;
|
||||
for (int c = 0; c < row.length(); c++) {
|
||||
char ch = row.charAt(c);
|
||||
keys.add(new KeyboardLayoutDescriptor.KeyInfo(
|
||||
String.valueOf(ch), (int) ch,
|
||||
offsetX + c * kw, rowY, kw, kh));
|
||||
}
|
||||
rowY += kh;
|
||||
}
|
||||
return new KeyboardLayoutDescriptor("en-US", keys, w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCandidatesReady(List<SwipeTypeCandidate> candidates) {
|
||||
if (!candidates.isEmpty()) {
|
||||
InputConnection ic = ime.getCurrentInputConnection();
|
||||
if (ic != null) ic.commitText(candidates.get(0).word + " ", 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(SwipeTypeError error) {
|
||||
Log.e("Minimal", error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read [API.md](API.md) for the full API reference
|
||||
- Read [ARCHITECTURE.md](ARCHITECTURE.md) to understand the recognition pipeline
|
||||
- Study the sample app for a complete working integration
|
||||
- Check the [CHANGELOG.md](../CHANGELOG.md) for the latest changes
|
||||
@@ -0,0 +1,241 @@
|
||||
# Developer Onboarding — libswipetype
|
||||
|
||||
> Get from zero to a working build + passing tests in under 15 minutes.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Tool | Version | Check |
|
||||
|------|---------|-------|
|
||||
| **Android Studio** or IntelliJ IDEA | 2023.1+ | `studio --version` |
|
||||
| **JDK** | 17+ | `java -version` |
|
||||
| **Android SDK** | API 34 (compileSdk) | SDK Manager |
|
||||
| **Android NDK** | 25.2.9519653 | SDK Manager → SDK Tools → NDK |
|
||||
| **CMake** | 3.18+ | `cmake --version` |
|
||||
| **Python** | 3.8+ | `python3 --version` (for dictionary generation) |
|
||||
| **Git** | 2.x | `git --version` |
|
||||
|
||||
Optional:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| **clang-format** | Code formatting (`.clang-format` in repo root) |
|
||||
| **adb** | On-device testing |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/libswipetype/libswipetype.git
|
||||
cd libswipetype
|
||||
```
|
||||
|
||||
### 2. Build & test the C++ core (standalone)
|
||||
|
||||
The C++ core has zero external dependencies (Google Test is fetched automatically).
|
||||
|
||||
```bash
|
||||
cd swipetype-core
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Debug -DSWIPETYPE_BUILD_TESTS=ON
|
||||
cmake --build . -j$(nproc)
|
||||
ctest --output-on-failure
|
||||
```
|
||||
|
||||
You should see **48 tests passed, 0 failed**.
|
||||
|
||||
### 3. Build the full Android project
|
||||
|
||||
Open the project root in Android Studio, or build from the command line:
|
||||
|
||||
```bash
|
||||
cd /path/to/libswipetype
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
This builds:
|
||||
- `swipetype-core` (via CMake, triggered by Gradle)
|
||||
- `swipetype-android` AAR
|
||||
- `adapters/heliboard` module
|
||||
- `sample-app` debug APK
|
||||
|
||||
### 4. Run the sample app
|
||||
|
||||
```bash
|
||||
# Install on a connected device or emulator
|
||||
./gradlew :sample-app:installDebug
|
||||
|
||||
# Or use Android Studio: Run → sample-app
|
||||
```
|
||||
|
||||
Then on the device:
|
||||
1. Open "SwipeType Demo" app
|
||||
2. Tap "Enable IME" → enable "SwipeType Sample IME" in Settings
|
||||
3. Return to the app
|
||||
4. Tap the text field → switch to the SwipeType keyboard via the globe icon
|
||||
5. Draw a swipe gesture across the keys
|
||||
|
||||
### 5. Generate a dictionary (optional)
|
||||
|
||||
The sample app already includes `en_us_sample.glide` in `res/raw/`. To regenerate or create a new dictionary:
|
||||
|
||||
```bash
|
||||
python3 scripts/gen_dict.py \
|
||||
--input test-data/en-us-full.tsv \
|
||||
--output sample-app/src/main/res/raw/en_us_sample.glide \
|
||||
--language en-US
|
||||
```
|
||||
|
||||
TSV format: `word<TAB>frequency` per line. No header row.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure at a Glance
|
||||
|
||||
```
|
||||
libswipetype/
|
||||
├── swipetype-core/ ← C++ library (the brain)
|
||||
├── swipetype-android/ ← Android AAR (JNI bridge)
|
||||
├── adapters/heliboard/ ← Reference adapter for HeliBoard
|
||||
├── sample-app/ ← Minimal IME demo
|
||||
├── scripts/ ← Dictionary generation
|
||||
├── test-data/ ← Test fixtures (layouts, gestures, word lists)
|
||||
├── docs/ ← Documentation (you are here)
|
||||
├── build.gradle ← Root Gradle build
|
||||
└── settings.gradle ← Module declarations
|
||||
```
|
||||
|
||||
For a detailed breakdown, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Coordinate System
|
||||
|
||||
Everything uses **density-independent pixels (dp)**, relative to the top-left corner of the keyboard view. The keyboard app provides key positions in dp; the user's touch events must also be in dp. This ensures the algorithm is screen-density-independent.
|
||||
|
||||
### Adapter Pattern
|
||||
|
||||
The library doesn't depend on any specific keyboard app. Instead, each keyboard provides an **adapter** implementing `SwipeTypeAdapter`:
|
||||
|
||||
```
|
||||
┌──────────────┐ SwipeTypeAdapter ┌──────────────┐
|
||||
│ HeliBoard │──────────────────────────│ SwipeType │
|
||||
│ FlorisBoard │ getKeyboardLayout() │ Engine │
|
||||
│ Your App │ onCandidatesReady() │ │
|
||||
└──────────────┘ onError() └──────────────┘
|
||||
```
|
||||
|
||||
### Recognition Pipeline (TL;DR)
|
||||
|
||||
1. Raw touch points → normalize to 64 equidistant points in [0,1] box
|
||||
2. Find start/end keys → filter dictionary candidates
|
||||
3. For each candidate: generate ideal path through key centers → compute DTW distance
|
||||
4. Rank by combined shape + frequency score → return top N
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
### C++ Tests (Google Test)
|
||||
|
||||
```bash
|
||||
cd swipetype-core/build
|
||||
ctest --output-on-failure
|
||||
```
|
||||
|
||||
Or run individual test suites:
|
||||
|
||||
```bash
|
||||
./swipetype-core-tests --gtest_filter="GestureEngineTest.*"
|
||||
./swipetype-core-tests --gtest_filter="PathProcessorTest.*"
|
||||
./swipetype-core-tests --gtest_filter="ScorerTest.*"
|
||||
```
|
||||
|
||||
### Android Tests
|
||||
|
||||
```bash
|
||||
# Unit tests (JVM)
|
||||
./gradlew :swipetype-android:test
|
||||
|
||||
# Connected tests (requires device/emulator)
|
||||
./gradlew :swipetype-android:connectedAndroidTest
|
||||
```
|
||||
|
||||
> **Note:** Some Java unit tests are currently disabled (skipped via `Assume.assumeTrue(false)`) pending JNI native library loading on the host JVM. The C++ tests provide full pipeline coverage.
|
||||
|
||||
---
|
||||
|
||||
## Common Development Workflows
|
||||
|
||||
### Modifying the recognition algorithm
|
||||
|
||||
1. Edit files in `swipetype-core/src/`
|
||||
2. Rebuild & test: `cd build && cmake --build . -j$(nproc) && ctest --output-on-failure`
|
||||
3. The Gradle build will automatically rebuild the native library for Android
|
||||
|
||||
### Adding a new word to the test dictionary
|
||||
|
||||
1. Add `word<TAB>frequency` to `test-data/en-us-full.tsv`
|
||||
2. Regenerate: `python3 scripts/gen_dict.py --input test-data/en-us-full.tsv --output sample-app/src/main/res/raw/en_us_sample.glide`
|
||||
3. Rebuild the sample app
|
||||
|
||||
### Tuning scoring parameters
|
||||
|
||||
Edit `ScoringConfig` defaults in `swipetype-core/include/swipetype/SwipeTypeTypes.h`, or configure at runtime:
|
||||
|
||||
```cpp
|
||||
ScoringConfig config;
|
||||
config.frequencyWeight = 0.20f; // reduce frequency influence
|
||||
config.lengthFilterTolerance = 4.0f; // widen length filter
|
||||
engine.configure(config);
|
||||
```
|
||||
|
||||
### Writing a new adapter
|
||||
|
||||
See [HOW_TO_WRITE_AN_ADAPTER.md](HOW_TO_WRITE_AN_ADAPTER.md).
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Logcat tags
|
||||
|
||||
| Tag | Source | What it shows |
|
||||
|-----|--------|---------------|
|
||||
| `SampleIME` | `SampleInputMethodService` | Layout size, gesture bounding box, top candidate |
|
||||
| `SwipeTypeEngine` | `SwipeTypeEngine.java` | Init, dict load, JNI errors |
|
||||
| `GestureLibJNI` | `GestureLibJNI.cpp` | Native-side errors, candidate counts |
|
||||
|
||||
### View gesture coordinates
|
||||
|
||||
The sample IME logs the gesture bounding box on every swipe:
|
||||
|
||||
```
|
||||
D SampleIME: Layout 360x260 dp | gesture x=[48..288] y=[26..134] 47 pts
|
||||
```
|
||||
|
||||
This helps verify coordinate alignment between the keyboard view and the engine.
|
||||
|
||||
### Examine DTW scores
|
||||
|
||||
Enable verbose logging in `GestureEngine.cpp` (define `SWIPETYPE_DEBUG`) to see per-candidate DTW scores, normalized values, and final confidence.
|
||||
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
1. **Native library name is `glide_jni`**, not `swipetype`. If you see `UnsatisfiedLinkError`, check that the `.so` files are in the correct `jniLibs/` directory.
|
||||
|
||||
2. **Dictionary must be loaded before `processGesture()`**. The `onInit()` callback fires only after `loadDictionary()` succeeds — do NOT call `loadDictionary()` from inside `onInit()` (infinite loop).
|
||||
|
||||
3. **Coordinates must be in dp**, not pixels. If candidates are wrong or empty, check that touch coordinates and layout key positions use the same unit.
|
||||
|
||||
4. **The C++ library is NOT thread-safe**. The Java `SwipeTypeEngine` handles synchronization, but if you call the C++ API directly, you must synchronize externally.
|
||||
|
||||
5. **Duplicate package `dev.swipetype`** exists in the codebase alongside `dev.dettmer.swipetype`. Both work identically — the duplicates will be consolidated in Phase 12.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Project-wide Gradle settings
|
||||
|
||||
# Enable AndroidX
|
||||
android.useAndroidX=true
|
||||
|
||||
# JVM settings
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
|
||||
# Android SDK/NDK
|
||||
# NDK version — minimum r25 required
|
||||
android.ndkVersion=25.2.9519653
|
||||
|
||||
# Enable parallel execution
|
||||
org.gradle.parallel=true
|
||||
|
||||
# Enable build cache
|
||||
org.gradle.caching=true
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
gen_dict.py — Convert a TSV word list to binary .glide dictionary format.
|
||||
|
||||
Usage:
|
||||
python3 gen_dict.py input.tsv output.glide --lang en-US
|
||||
|
||||
Input format (TSV):
|
||||
word<TAB>frequency
|
||||
hello\t100000
|
||||
world\t95000
|
||||
...
|
||||
|
||||
One word per line. Frequency is a positive integer (higher = more common).
|
||||
Lines starting with # are comments. Empty lines are skipped.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Constants matching SwipeTypeTypes.h
|
||||
DICT_MAGIC = 0x474C4944 # "GLID"
|
||||
DICT_VERSION = 1
|
||||
DICT_HEADER_SIZE = 32
|
||||
MAX_WORD_LENGTH = 64
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert TSV word list to binary .glide dictionary format."
|
||||
)
|
||||
parser.add_argument("input", help="Input TSV file (word<TAB>frequency)")
|
||||
parser.add_argument("output", help="Output .glide binary file")
|
||||
parser.add_argument(
|
||||
"--lang", default="en-US",
|
||||
help="BCP 47 language tag (default: en-US)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sort", action="store_true", default=True,
|
||||
help="Sort entries alphabetically (default: true)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-sort", action="store_false", dest="sort",
|
||||
help="Don't sort entries"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--proper-nouns", action="store_true", default=False,
|
||||
help="Mark capitalized words as proper nouns"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_tsv(filepath):
|
||||
"""Read a TSV file and return list of (word, frequency, flags) tuples."""
|
||||
entries = []
|
||||
line_num = 0
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line_num += 1
|
||||
line = line.strip()
|
||||
|
||||
# Skip empty lines and comments
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
parts = line.split("\t")
|
||||
if len(parts) < 2:
|
||||
print(f"WARNING: Line {line_num}: expected 'word\\tfrequency', got '{line}'",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
|
||||
word = parts[0].strip()
|
||||
try:
|
||||
frequency = int(parts[1].strip())
|
||||
except ValueError:
|
||||
print(f"WARNING: Line {line_num}: invalid frequency '{parts[1]}'",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
|
||||
# Validate
|
||||
word_bytes = word.encode("utf-8")
|
||||
if len(word_bytes) == 0:
|
||||
print(f"WARNING: Line {line_num}: empty word, skipping",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
|
||||
if len(word_bytes) > MAX_WORD_LENGTH:
|
||||
print(f"WARNING: Line {line_num}: word '{word}' exceeds {MAX_WORD_LENGTH} bytes, skipping",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
|
||||
if frequency < 0:
|
||||
print(f"WARNING: Line {line_num}: negative frequency for '{word}', using 0",
|
||||
file=sys.stderr)
|
||||
frequency = 0
|
||||
|
||||
if frequency > 0xFFFFFFFF:
|
||||
print(f"WARNING: Line {line_num}: frequency too large for '{word}', clamping",
|
||||
file=sys.stderr)
|
||||
frequency = 0xFFFFFFFF
|
||||
|
||||
# Flags (optional 3rd column)
|
||||
flags = 0
|
||||
if len(parts) >= 3:
|
||||
flag_str = parts[2].strip().lower()
|
||||
if "proper" in flag_str:
|
||||
flags |= 0x01
|
||||
if "profanity" in flag_str:
|
||||
flags |= 0x02
|
||||
|
||||
entries.append((word, frequency, flags))
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def write_glide(entries, output_path, language_tag, sorted_flag):
|
||||
"""Write entries to a binary .glide file."""
|
||||
lang_bytes = language_tag.encode("utf-8")
|
||||
if len(lang_bytes) > 18:
|
||||
print(f"ERROR: Language tag '{language_tag}' exceeds 18 bytes",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Compute header flags
|
||||
hdr_flags = 0
|
||||
if sorted_flag:
|
||||
hdr_flags |= 0x01 # bit 0: sorted alphabetically
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
# === Write header (32 bytes) ===
|
||||
header = bytearray(DICT_HEADER_SIZE)
|
||||
|
||||
# Magic (bytes 0-3)
|
||||
struct.pack_into("<I", header, 0, DICT_MAGIC)
|
||||
# Version (bytes 4-5)
|
||||
struct.pack_into("<H", header, 4, DICT_VERSION)
|
||||
# Flags (bytes 6-7)
|
||||
struct.pack_into("<H", header, 6, hdr_flags)
|
||||
# Entry count (bytes 8-11)
|
||||
struct.pack_into("<I", header, 8, len(entries))
|
||||
# Language tag length (bytes 12-13)
|
||||
struct.pack_into("<H", header, 12, len(lang_bytes))
|
||||
# Language tag (bytes 14+)
|
||||
header[14:14 + len(lang_bytes)] = lang_bytes
|
||||
# Remaining bytes stay zero (padding to 32-byte boundary)
|
||||
|
||||
f.write(header)
|
||||
|
||||
# === Write entries ===
|
||||
for word, frequency, entry_flags in entries:
|
||||
word_bytes = word.encode("utf-8")
|
||||
word_len = len(word_bytes)
|
||||
|
||||
# Word length (1 byte)
|
||||
f.write(struct.pack("<B", word_len))
|
||||
# Word bytes (variable length)
|
||||
f.write(word_bytes)
|
||||
# Frequency (4 bytes, little-endian uint32)
|
||||
f.write(struct.pack("<I", frequency))
|
||||
# Flags (1 byte)
|
||||
f.write(struct.pack("<B", entry_flags))
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
# Read input
|
||||
print(f"Reading: {args.input}")
|
||||
entries = read_tsv(args.input)
|
||||
|
||||
if not entries:
|
||||
print("ERROR: No valid entries found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Mark proper nouns if requested
|
||||
if args.proper_nouns:
|
||||
new_entries = []
|
||||
for word, freq, flags in entries:
|
||||
if word[0].isupper():
|
||||
flags |= 0x01
|
||||
new_entries.append((word.lower(), freq, flags))
|
||||
entries = new_entries
|
||||
else:
|
||||
# Lowercase all words
|
||||
entries = [(word.lower(), freq, flags) for word, freq, flags in entries]
|
||||
|
||||
# Remove duplicates (keep highest frequency)
|
||||
seen = {}
|
||||
for word, freq, flags in entries:
|
||||
if word not in seen or freq > seen[word][1]:
|
||||
seen[word] = (word, freq, flags)
|
||||
entries = list(seen.values())
|
||||
|
||||
# Sort if requested
|
||||
if args.sort:
|
||||
entries.sort(key=lambda e: e[0])
|
||||
|
||||
# Write output
|
||||
print(f"Writing: {args.output}")
|
||||
write_glide(entries, args.output, args.lang, args.sort)
|
||||
|
||||
# Summary
|
||||
file_size = os.path.getsize(args.output)
|
||||
max_freq = max(e[1] for e in entries) if entries else 0
|
||||
min_freq = min(e[1] for e in entries) if entries else 0
|
||||
|
||||
print(f"\n=== Dictionary Summary ===")
|
||||
print(f" Language: {args.lang}")
|
||||
print(f" Words: {len(entries)}")
|
||||
print(f" File size: {file_size:,} bytes ({file_size / 1024:.1f} KB)")
|
||||
print(f" Sorted: {'yes' if args.sort else 'no'}")
|
||||
print(f" Freq range: {min_freq:,} — {max_freq:,}")
|
||||
print(f" Format: GLID v{DICT_VERSION}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "=== libswipetype Test Runner ==="
|
||||
echo ""
|
||||
|
||||
# ---- Core C++ Tests ----
|
||||
echo ">>> Building and running swipetype-core tests..."
|
||||
cd "$ROOT_DIR/swipetype-core"
|
||||
|
||||
cmake -B build \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DGLIDE_BUILD_TESTS=ON \
|
||||
2>&1 | tail -5
|
||||
|
||||
cmake --build build --parallel "$(nproc)" 2>&1 | tail -5
|
||||
|
||||
echo ">>> Running C++ unit tests..."
|
||||
cd build
|
||||
ctest --output-on-failure --verbose
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
echo ""
|
||||
echo ">>> Core tests complete."
|
||||
echo ""
|
||||
|
||||
# ---- Android Tests (if Gradle available) ----
|
||||
if command -v ./gradlew &> /dev/null || [ -f "$ROOT_DIR/gradlew" ]; then
|
||||
echo ">>> Running Android unit tests..."
|
||||
cd "$ROOT_DIR"
|
||||
chmod +x gradlew
|
||||
./gradlew test --stacktrace 2>&1 | tail -20
|
||||
echo ">>> Android tests complete."
|
||||
else
|
||||
echo ">>> Skipping Android tests (no gradlew found)."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== All tests finished ==="
|
||||
@@ -0,0 +1,37 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
project(swipetype-android-jni VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# ============================================================================
|
||||
# JNI Shared Library
|
||||
# ============================================================================
|
||||
|
||||
add_library(glide_jni SHARED
|
||||
src/main/cpp/GestureLibJNI.cpp
|
||||
)
|
||||
|
||||
# Link against swipetype-core (built from sibling directory)
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../swipetype-core
|
||||
${CMAKE_CURRENT_BINARY_DIR}/swipetype-core)
|
||||
|
||||
target_link_libraries(glide_jni
|
||||
PRIVATE
|
||||
swipetype-core
|
||||
log # Android logging
|
||||
)
|
||||
|
||||
target_include_directories(glide_jni
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../swipetype-core/include
|
||||
)
|
||||
|
||||
# 16 KB page-size alignment required for Android 15+ (API 35+)
|
||||
target_link_options(glide_jni PRIVATE -Wl,-z,max-page-size=16384)
|
||||
|
||||
# Strip in release for smaller .so
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
target_link_options(glide_jni PRIVATE -s)
|
||||
endif()
|
||||
@@ -0,0 +1,66 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'dev.dettmer.swipetype.android'
|
||||
compileSdk 34
|
||||
ndkVersion '27.0.12077973'
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags '-std=c++17 -O2 -Wall -Wextra'
|
||||
arguments '-DANDROID_STL=c++_static',
|
||||
'-DSWIPETYPE_BUILD_TESTS=OFF'
|
||||
}
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable true
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'CMakeLists.txt'
|
||||
version '3.18.1+'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs = ['src/main/java']
|
||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||
}
|
||||
test {
|
||||
java.srcDirs = ['src/test/java']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:5.8.0'
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="dev.dettmer.swipetype.android">
|
||||
<!-- Library module — no application or activities -->
|
||||
</manifest>
|
||||
@@ -0,0 +1,318 @@
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <android/log.h>
|
||||
|
||||
#include "swipetype/GestureEngine.h"
|
||||
#include "swipetype/GesturePath.h"
|
||||
#include "swipetype/GestureCandidate.h"
|
||||
#include "swipetype/KeyboardLayout.h"
|
||||
#include "swipetype/SwipeTypeTypes.h"
|
||||
|
||||
#define LOG_TAG "SwipeTypeJNI"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
/**
|
||||
* @file GestureLibJNI.cpp
|
||||
* @brief JNI bridge between SwipeTypeEngine.java and swipetype-core C++ library.
|
||||
*
|
||||
* This file:
|
||||
* - Converts Java arrays to C++ data structures
|
||||
* - Manages GestureEngine lifetime via opaque handles (jlong pointers)
|
||||
* - Converts C++ results back to Java arrays/strings
|
||||
* - Handles all JNI exceptions to prevent native crashes from reaching Java
|
||||
*
|
||||
* Threading: All methods assume external synchronization (provided by
|
||||
* SwipeTypeEngine.java's synchronized blocks).
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Build a KeyboardLayout from JNI arrays.
|
||||
*/
|
||||
static swipetype::KeyboardLayout buildLayout(
|
||||
JNIEnv* env,
|
||||
jfloatArray keyPositionsX, jfloatArray keyPositionsY,
|
||||
jfloatArray keyWidths, jfloatArray keyHeights,
|
||||
jintArray keyCodePoints, jint keyCount,
|
||||
jfloat layoutWidth, jfloat layoutHeight,
|
||||
jstring languageTag) {
|
||||
|
||||
swipetype::KeyboardLayout layout;
|
||||
|
||||
// Language tag
|
||||
if (languageTag != nullptr) {
|
||||
const char* langStr = env->GetStringUTFChars(languageTag, nullptr);
|
||||
if (langStr) {
|
||||
layout.languageTag = std::string(langStr);
|
||||
env->ReleaseStringUTFChars(languageTag, langStr);
|
||||
}
|
||||
}
|
||||
|
||||
layout.layoutWidth = layoutWidth;
|
||||
layout.layoutHeight = layoutHeight;
|
||||
|
||||
if (keyCount <= 0) return layout;
|
||||
|
||||
jfloat* xArr = env->GetFloatArrayElements(keyPositionsX, nullptr);
|
||||
jfloat* yArr = env->GetFloatArrayElements(keyPositionsY, nullptr);
|
||||
jfloat* wArr = env->GetFloatArrayElements(keyWidths, nullptr);
|
||||
jfloat* hArr = env->GetFloatArrayElements(keyHeights, nullptr);
|
||||
jint* cpArr = env->GetIntArrayElements(keyCodePoints, nullptr);
|
||||
|
||||
if (xArr && yArr && wArr && hArr && cpArr) {
|
||||
layout.keys.reserve(keyCount);
|
||||
for (jint i = 0; i < keyCount; ++i) {
|
||||
swipetype::KeyDescriptor key;
|
||||
key.centerX = xArr[i];
|
||||
key.centerY = yArr[i];
|
||||
key.width = wArr[i];
|
||||
key.height = hArr[i];
|
||||
key.codePoint = cpArr[i];
|
||||
key.label = (cpArr[i] > 0 && cpArr[i] < 128)
|
||||
? std::string(1, static_cast<char>(cpArr[i]))
|
||||
: std::string();
|
||||
layout.keys.push_back(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (xArr) env->ReleaseFloatArrayElements(keyPositionsX, xArr, JNI_ABORT);
|
||||
if (yArr) env->ReleaseFloatArrayElements(keyPositionsY, yArr, JNI_ABORT);
|
||||
if (wArr) env->ReleaseFloatArrayElements(keyWidths, wArr, JNI_ABORT);
|
||||
if (hArr) env->ReleaseFloatArrayElements(keyHeights, hArr, JNI_ABORT);
|
||||
if (cpArr) env->ReleaseIntArrayElements(keyCodePoints, cpArr, JNI_ABORT);
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JNI Method Implementations
|
||||
// ============================================================================
|
||||
|
||||
extern "C" {
|
||||
|
||||
/**
|
||||
* Initialize the native engine with layout and dictionary file path.
|
||||
*
|
||||
* @return Native handle (cast GestureEngine* to jlong), or 0 on failure.
|
||||
*/
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_dev_dettmer_swipetype_android_SwipeTypeEngine_nativeInit(
|
||||
JNIEnv* env, jclass /*clazz*/,
|
||||
jfloatArray keyPositionsX, jfloatArray keyPositionsY,
|
||||
jfloatArray keyWidths, jfloatArray keyHeights,
|
||||
jintArray keyCodePoints, jint keyCount,
|
||||
jfloat layoutWidth, jfloat layoutHeight,
|
||||
jstring languageTag, jstring dictPath) {
|
||||
|
||||
try {
|
||||
swipetype::KeyboardLayout layout = buildLayout(
|
||||
env, keyPositionsX, keyPositionsY, keyWidths, keyHeights,
|
||||
keyCodePoints, keyCount, layoutWidth, layoutHeight, languageTag);
|
||||
|
||||
const char* pathStr = env->GetStringUTFChars(dictPath, nullptr);
|
||||
std::string dictPathStr(pathStr ? pathStr : "");
|
||||
if (pathStr) env->ReleaseStringUTFChars(dictPath, pathStr);
|
||||
|
||||
auto* engine = new swipetype::GestureEngine();
|
||||
if (!engine->init(layout, dictPathStr)) {
|
||||
LOGE("Failed to initialize engine: %s",
|
||||
engine->getLastError().message.c_str());
|
||||
delete engine;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOGI("Engine initialized with dictionary: %s", dictPathStr.c_str());
|
||||
return reinterpret_cast<jlong>(engine);
|
||||
} catch (...) {
|
||||
LOGE("Exception in nativeInit");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with dictionary data from memory (byte array).
|
||||
*/
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_dev_dettmer_swipetype_android_SwipeTypeEngine_nativeInitWithData(
|
||||
JNIEnv* env, jclass /*clazz*/,
|
||||
jfloatArray keyPositionsX, jfloatArray keyPositionsY,
|
||||
jfloatArray keyWidths, jfloatArray keyHeights,
|
||||
jintArray keyCodePoints, jint keyCount,
|
||||
jfloat layoutWidth, jfloat layoutHeight,
|
||||
jstring languageTag, jbyteArray dictData) {
|
||||
|
||||
try {
|
||||
swipetype::KeyboardLayout layout = buildLayout(
|
||||
env, keyPositionsX, keyPositionsY, keyWidths, keyHeights,
|
||||
keyCodePoints, keyCount, layoutWidth, layoutHeight, languageTag);
|
||||
|
||||
jsize dataSize = env->GetArrayLength(dictData);
|
||||
jbyte* dataPtr = env->GetByteArrayElements(dictData, nullptr);
|
||||
|
||||
auto* engine = new swipetype::GestureEngine();
|
||||
bool ok = engine->initWithData(layout,
|
||||
reinterpret_cast<const uint8_t*>(dataPtr),
|
||||
static_cast<size_t>(dataSize));
|
||||
|
||||
env->ReleaseByteArrayElements(dictData, dataPtr, JNI_ABORT);
|
||||
|
||||
if (!ok) {
|
||||
LOGE("Failed to initialize engine from memory: %s",
|
||||
engine->getLastError().message.c_str());
|
||||
delete engine;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOGI("Engine initialized from memory (%d bytes)", (int)dataSize);
|
||||
return reinterpret_cast<jlong>(engine);
|
||||
} catch (...) {
|
||||
LOGE("Exception in nativeInitWithData");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognize a gesture path and write results to output arrays.
|
||||
*
|
||||
* @return Number of candidates written, or -1 on error.
|
||||
*/
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_dev_dettmer_swipetype_android_SwipeTypeEngine_nativeRecognize(
|
||||
JNIEnv* env, jclass /*clazz*/,
|
||||
jlong handle,
|
||||
jfloatArray xCoords, jfloatArray yCoords, jlongArray timestamps,
|
||||
jint pointCount, jint maxCandidates,
|
||||
jobjectArray outWords, jfloatArray outScores, jintArray outFlags) {
|
||||
|
||||
try {
|
||||
auto* engine = reinterpret_cast<swipetype::GestureEngine*>(handle);
|
||||
if (engine == nullptr) return -1;
|
||||
|
||||
// Build raw path from JNI arrays
|
||||
jfloat* xArr = env->GetFloatArrayElements(xCoords, nullptr);
|
||||
jfloat* yArr = env->GetFloatArrayElements(yCoords, nullptr);
|
||||
jlong* tArr = env->GetLongArrayElements(timestamps, nullptr);
|
||||
|
||||
swipetype::RawGesturePath raw;
|
||||
raw.points.reserve(pointCount);
|
||||
if (xArr && yArr && tArr) {
|
||||
for (jint i = 0; i < pointCount; ++i) {
|
||||
raw.points.emplace_back(xArr[i], yArr[i], static_cast<int64_t>(tArr[i]));
|
||||
}
|
||||
}
|
||||
|
||||
if (xArr) env->ReleaseFloatArrayElements(xCoords, xArr, JNI_ABORT);
|
||||
if (yArr) env->ReleaseFloatArrayElements(yCoords, yArr, JNI_ABORT);
|
||||
if (tArr) env->ReleaseLongArrayElements(timestamps, tArr, JNI_ABORT);
|
||||
|
||||
// Recognize
|
||||
auto candidates = engine->recognize(raw, maxCandidates);
|
||||
int count = static_cast<int>(
|
||||
std::min(candidates.size(), static_cast<size_t>(maxCandidates)));
|
||||
|
||||
// Debug: log all candidates with scores
|
||||
LOGD("recognize: %d pts -> %d candidates", (int)pointCount, count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
LOGD(" #%d %-12s conf=%.4f dtw=%.4f freq=%.4f",
|
||||
i + 1,
|
||||
candidates[i].word.c_str(),
|
||||
candidates[i].confidence,
|
||||
candidates[i].dtwScore,
|
||||
candidates[i].frequencyScore);
|
||||
}
|
||||
|
||||
// Write results back to Java arrays
|
||||
jfloat* scoreArr = env->GetFloatArrayElements(outScores, nullptr);
|
||||
jint* flagArr = env->GetIntArrayElements(outFlags, nullptr);
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
jstring word = env->NewStringUTF(candidates[i].word.c_str());
|
||||
env->SetObjectArrayElement(outWords, i, word);
|
||||
env->DeleteLocalRef(word);
|
||||
|
||||
if (scoreArr) scoreArr[i] = candidates[i].confidence;
|
||||
if (flagArr) flagArr[i] = static_cast<jint>(candidates[i].sourceFlags);
|
||||
}
|
||||
|
||||
if (scoreArr) env->ReleaseFloatArrayElements(outScores, scoreArr, 0);
|
||||
if (flagArr) env->ReleaseIntArrayElements(outFlags, flagArr, 0);
|
||||
|
||||
return count;
|
||||
} catch (...) {
|
||||
LOGE("Exception in nativeRecognize");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update keyboard layout without reloading dictionary.
|
||||
*/
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_dettmer_swipetype_android_SwipeTypeEngine_nativeUpdateLayout(
|
||||
JNIEnv* env, jclass /*clazz*/,
|
||||
jlong handle,
|
||||
jfloatArray keyPositionsX, jfloatArray keyPositionsY,
|
||||
jfloatArray keyWidths, jfloatArray keyHeights,
|
||||
jintArray keyCodePoints, jint keyCount,
|
||||
jfloat layoutWidth, jfloat layoutHeight) {
|
||||
|
||||
try {
|
||||
auto* engine = reinterpret_cast<swipetype::GestureEngine*>(handle);
|
||||
if (engine == nullptr) return JNI_FALSE;
|
||||
|
||||
swipetype::KeyboardLayout layout = buildLayout(
|
||||
env, keyPositionsX, keyPositionsY, keyWidths, keyHeights,
|
||||
keyCodePoints, keyCount, layoutWidth, layoutHeight, nullptr);
|
||||
|
||||
return engine->updateLayout(layout) ? JNI_TRUE : JNI_FALSE;
|
||||
} catch (...) {
|
||||
LOGE("Exception in nativeUpdateLayout");
|
||||
return JNI_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the engine and free resources.
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_dettmer_swipetype_android_SwipeTypeEngine_nativeShutdown(
|
||||
JNIEnv* /*env*/, jclass /*clazz*/, jlong handle) {
|
||||
auto* engine = reinterpret_cast<swipetype::GestureEngine*>(handle);
|
||||
if (engine != nullptr) {
|
||||
engine->shutdown();
|
||||
delete engine;
|
||||
LOGI("Native engine shut down");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if engine is initialized.
|
||||
*/
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_dev_dettmer_swipetype_android_SwipeTypeEngine_nativeIsInitialized(
|
||||
JNIEnv* /*env*/, jclass /*clazz*/, jlong handle) {
|
||||
auto* engine = reinterpret_cast<swipetype::GestureEngine*>(handle);
|
||||
if (engine == nullptr) return JNI_FALSE;
|
||||
return engine->isInitialized() ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JNI Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
JNIEXPORT jint JNI_OnLoad(JavaVM* /*vm*/, void* /*reserved*/) {
|
||||
LOGI("libswipetype JNI loaded");
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNI_OnUnload(JavaVM* /*vm*/, void* /*reserved*/) {
|
||||
LOGI("libswipetype JNI unloaded");
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||