Compare commits

...
300 Commits
Author SHA1 Message Date
harley 15f93cade1 Fix voice typing on Android 16: detect standalone voice IMEs
Google Voice Typing on Android 16+ is a standalone IME with zero
subtypes, so the old code (scanning subtypes for mode='voice')
never found it. Also detect IMEs with getSubtypeCount()==0 and
handle null subtype in switchInputMethod() and get_display_name().
2026-07-08 01:45:35 -04:00
harley 345f3cdbd6 Clean up build artifacts from git 2026-07-07 15:03:56 -04:00
harley 7aaebb5bfd Wire swipe typing lifecycle into Keyboard2
- Initialize MornKeyboardSwipeAdapter on Keyboard2.onCreate()
- Load en_US.gilde dictionary from res/raw/
- Create SwipeInputHandler and wire to Keyboard2View in create_keyboard_view()
- Add MornSwipeReceiver that commits top candidate via InputConnection
- Call notifySwipeLayoutChanged() on layout changes
- Clean up on onDestroy()
- Full debug build verified (82 tasks)
2026-07-07 15:03:47 -04:00
harley e17b7db68a Add .gitignore, remove build artifacts 2026-07-07 15:01:42 -04:00
harley 9bdfaa56e6 Package rename to juloo.mornkeyboard2 + import libswipetype 2026-07-07 15:01:37 -04:00
harley eb9a5103c2 Increase key spacing margins: horizontal 13, vertical 6 2026-07-07 06:12:12 -04:00
harley f59603c90b Full layout overhaul: Fn grid, bottom row reorg, symbol moves, dictionary, compose relocation 2026-07-07 05:59:17 -04:00
harley 4534df8bcb Layout: compose on pg key, keyboard height 0.81, smaller sublabels 2026-07-07 04:50:38 -04:00
harley ecdb43a593 Layout: compose on pg key, keyboard height 0.81, smaller sublabels 2026-07-07 04:50:29 -04:00
harley 939e8da252 Save state before swipe engine work 2026-07-07 02:59:52 -04:00
harley e2d5b47c7c Layout overhaul: symbol moves, bottom row reorg, Tux meta key
- '!' moved to bottom-left of '1' (q sw), removed from e ne
- Dash on o sw, underscore on o se
- ':' on l sw (replaced backslash)
- Bottom row: Ctrl | Alt | Space | Fn | Enter
- Fn moved right of space bar, corner swipes cleaned up
- Voice typing moved from Enter to Fn bottom-left (key6)
- Clipboard on Ctrl bottom-left (key6)
- New 'tux' key: META modifier displayed as Tux penguin (0xE002)
  from special_font/002.svg, toggleable (latch+double-tap lock)
- Enter only has action corner (key2), no voice typing
- Config (settings) kept on Fn up-swipe (key1)
2026-07-07 02:18:32 -04:00
harley 96966332ce Bump long-press timeout 600→700ms 2026-07-07 02:08:47 -04:00
harley b034191a1c Move numbers to left side of top row, restore shift next to Z, add config to Fn
- Top row numbers moved from ne (top-right) to nw (top-left) per request
- Existing nw symbols (tilde, !) shifted to ne positions
- Shift restored in z-row (normal key width)
- Backspace now normal key width (no more 1.5x)
- Fn key now has corner swipes: config, change_method, emoji, switch_numeric
- Bottom row: Fn | Ctrl | Alt | Space | Enter
2026-07-07 02:02:35 -04:00
harley b86481461a Rewrite joypad as direction-lock + auto-repeat
- Joypad no longer uses displacement-accumulation (useless on space bar).
- After long-press space bar, determine direction from finger offset vs origin.
- Direction locks, fires one cursor event, then auto-repeats at
  longPressInterval rate via _longpress_handler.
- Changing finger direction updates the lock (fires in new dir immediately).
- Returning finger to dead zone stops auto-repeat.
- Removed SPEED_VERTICAL_MULT and startup threshold:space bar has no room.
- Space bar icon changed to 0xE001 (4-directional arrows).
2026-07-07 01:55:34 -04:00
harley 5fbd21c7d9 Move joypad to space bar long-press, remove separate joystick key
- Space bar long-press now activates Joypad cursor mode
  (long-press detected in handleLongPress before key-repeat)
- Removed separate cursor_joystick key from bottom row
- Widened space bar (4.2 → 6.0) to compensate
- Bottom row: Shift | Fn | Ctrl | Alt | Space (joypad on long-press) | Enter
2026-07-07 01:49:56 -04:00
harley b6618444a2 Fix: remove shift from z-row, rebuild special font with joystick icon
- Removed redundant shift key from latn_qwerty_us.xml third row
  (shift is now on the bottom row only)
- Rebuilt assets/special_font.ttf to include 001.svg (4-directional
  joystick icon at codepoint 0xE001)
2026-07-07 01:45:14 -04:00
harley 4a05bf54d5 Add 2D joypad cursor slider + custom bottom row layout 2026-07-07 01:38:43 -04:00
Jules Aguillon 9c70f1113e Suggestion bar to the middle of split layouts (#1350)
When the layout is split, insert a middle column showing the
complete_first, complete_second, etc.. keys.
These keys are rendered without borders and background with a new role.

The suggestion bar is hidden for split layouts. The intention is to gain
much needed vertical space.
2026-07-05 00:59:21 +02:00
Jules Aguillon c0a4bca8e2 Fix compose-space sequences not working (#1352)
* Add tests

* Fix compose-space sequences not working

KeyModifier didn't pass editing keys to ComposeKey.

* Cleanup test code

* Show the nbsp symbol when typing compose-space-space
2026-07-04 23:04:34 +02:00
Jules Aguillon b6ba00a1f6 More informative "download failed" message (#1353)
Ask users to allow internet access, which will make sense to users of
grapheneOS and other systems that restrict internet access to apps.
2026-07-04 22:43:26 +02:00
Jules Aguillon 43550a3ad1 Fix the default layout for Hebrew (#1351)
The default layout for Hebrew was not specified correctly.
2026-07-03 03:01:53 +02:00
Jules Aguillon b73ac529e8 Small improvements to the complete keys (#1349)
* Make complete keys not repeatable

* Fix complete keys not updated when suggestions change

* Code cleanup
2026-07-03 02:50:21 +02:00
Jules Aguillon f192dfcb32 Add landscape variant for the number and pin layouts (#1341)
The number pane and the pin entry layouts are split in landscape
orientation. Instead of inserting empty space in the middle of the
layout, the numpad is duplicated. The other keys are not duplicated.
2026-07-01 01:09:36 +02:00
Jules Aguillon 4ab921f112 Add keys for entering the current suggestions (#1176)
This adds the new "Stateful" kind of keys, for which the symbol is
dynamically computed from the app's state. This is implemented with a
hook directly in KeyValue.

3 keys are added for accessing the 3 words on the candidates view.
2026-06-30 16:57:27 +02:00
Jules Aguillon 44dcf6e753 Split layout on tablets and foldables (#1339)
Split the layout on wide screens by default, not just in landscape
orientation.
2026-06-27 19:08:01 +02:00
Jules Aguillon ccd9568e3d CI: Set gradle version (#1340)
Since gradle release, the CI fails due to the android plugin.
2026-06-25 00:52:12 +02:00
Jules Aguillon 21633f22fd Split layout in landscape mode (#1334)
* Automatically split layouts in landscape

This inserts empty space in the center of layouts when in landscape
orientation. The keys are smaller and pushed to the edges of the screen.
This way, the fingers don't have to reach the center of the screen,
which is inconvenient in landscape orientation.

* Duplicate keys that are on the middle

Keys that are almost aligned on the middle of the screen are duplicated
on both sides. These keys are most probably hit with both thumbs.
This is especially useful for large keys like the space bar on the
bottom row.

* Split the bottom row

* Add an option to control layout splitting
2026-06-23 21:33:30 +02:00
Lokesh Kumar c6a568a74b add new item on top for history view as well (#1330)
* when pinning history item, keep it on top

* add new item on top of history as well
2026-06-20 17:50:47 +02:00
Jules Aguillon 241e034a3f Change the appareance of some keys (#896)
* Different color for action keys and space bar

Keys can be made darker or dimmer individually in layouts.
This is done by setting the new 'role' attributes. Roles are "normal",
"action" and "space_bar".

The "action" role is intended to be used for keys like shift, backspace
and other keys from the bottom row.

Themes and special layouts are updated.

* Set 'role="action"' for shift and backspace keys, the number row and numpad.

* check_layout.py: Check that some keys have a role
2026-06-20 14:49:46 +02:00
Lokesh Kumar 27329091af add hide_self key to hide keyboard (#1329) 2026-06-14 12:57:06 +02:00
Lokesh Kumar a7645c3934 when pinning history item, keep it on top (#1328) 2026-06-14 12:50:21 +02:00
Jules Aguillon 374c47af9d Fix exclusion rects overlapping with the navigation bar (#1326)
Since SDK 29, calling setSystemGestureExclusionRects is mandatory to
continue supporting the keyboard's gestures but since SDK 30, this is
done automatically.
2026-06-14 12:14:08 +02:00
Jules Aguillon 49627b63e3 Add option to control behavior with physical keyboard (#1324)
The option can be set to "Show everything" to show the keyboard even
when a physical keyboard is present.
This is a list preference to allow adding a "Show suggestions" option
later.
2026-06-13 15:34:42 +02:00
Jules Aguillon 2a14ec777c Release 2.0.4 (55) 2026-05-25 00:43:37 +02:00
Jules Aguillonandsolokot 6b6491c47a Integrate changes made in Weblate (#1307)
Co-authored-by: solokot <solokot@gmail.com>
2026-05-25 00:39:23 +02:00
Jules Aguillon dbe09902d4 Add missing dictionary for English (#1304)
* gen_method_xml.py: Add missing dictionary for English

* gen_method_xml.py: Propagate dictionary specified in default locales

For example, propagate the dictionary specified for 'en' to 'en-IN',
'en-NG', etc..

* Use the dictionary en_US for locale en_CA
2026-05-25 00:05:05 +02:00
+3 ff769489c3 Integrate changes made in Weblate (#1302)
Co-authored-by: Chasm Solacer <chasmsolacer@users.noreply.hosted.weblate.org>
Co-authored-by: E. Ta. <eta00823@gmail.com>
Co-authored-by: Edgars Andersons <Edgars+Weblate@gaitenis.id.lv>
Co-authored-by: Guzleon <guzleon@yahoo.es>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Janosch <janosch.walde@posteo.de>
Co-authored-by: Joppla <joppla@gmx.com>
Co-authored-by: Mehrdad Shaeri <mehrdad.shaeri@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Summer-Box1228 <tingrong1228@gmail.com>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
2026-05-23 20:41:16 +02:00
Jules Aguillon bfdb1a9604 Remove comments in translations files (#1301)
This helps reduce conflicts when merging changes made in Weblate.
2026-05-23 20:33:09 +02:00
Jules Aguillon 0eb0e6b76f Show the current locale when no dictionary is installed (#1300)
* Refactor: Move DeviceLocales instance to Config

Allows accesses from CandidatesView.

* Show the current locale when no dictionary is installed

This helps understand why installing a dictionary doesn't remove the
message asking to install a dictionary.

The "Install" button is removed and the message is more informative.

* Update translation files
2026-05-23 20:20:26 +02:00
Jules Aguillon 76ee5b2134 Add a background color under suggestions (#1299)
* Fix unwanted mutation of background drawable

* Add a background color under suggestions

Suggestions might be unreadable when the keyboard opacity is very low
(eg. black on black).

The new background is normally invisible, unless the opacity option is
not set to 100%. The corner are rounded like the keys on the keyboard.

* Add themes attributes for suggestion background

Future themes can use them.
2026-05-23 19:59:01 +02:00
Jules Aguillon b81f4acac9 Suggestions: Add digits and ' to word characters (#1298)
This adds support for English words (like "don't") and for correcting
typos containing digits (eg. "s9mething" → "something").
2026-05-23 15:30:48 +02:00
Jules Aguillon 7e19b1f68e Hide keyboard when physical keyboard is connected (#1295)
This is broken since 958900ca
2026-05-20 21:49:12 +02:00
Jules Aguillon e64da8eefe suggestions: Reduce text size and add padding (#1296) 2026-05-20 01:03:36 +02:00
AndreiD049 8532acf814 fix: inclusive check for word boundaries (#1281) 2026-05-20 01:01:34 +02:00
Jules Aguillon aa11bb25fd Avoid Github redirection when downloading dictionaries (#1291)
This removes a domain from connection logs and avoids an unecessary
redirection.
2026-05-17 13:01:39 +02:00
Jules Aguillon 779d438fa0 README: Mention keepandroidopen.org (#1292) 2026-05-14 13:49:27 +02:00
Jules Aguillon b807f217a0 Release 2.0.3 (54) 2026-05-06 23:29:15 +02:00
Jules Aguillon 9d0a849d34 Make building libcdict reproducible (#1282)
This is needed to release on F-Droid.
2026-05-06 23:25:37 +02:00
Jules Aguillon 267894b5ff Release 2.0.2 (53) 2026-05-06 01:29:04 +02:00
Jules Aguillon 56edba7b18 Remove mention of network access from store description (#1280)
This part of the store description is no longer true, as the application
makes a network request when downloading a dictionary.
2026-05-06 01:16:47 +02:00
alotbsol555 11b735bfe8 Update README (#1279)
Removed mention of network requests from the README, due to dictionary downloads.
2026-05-06 01:03:47 +02:00
7587c68d7f Merge weblate translations before 2.0.2 (#1277)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Juan M Sevilla <jumase@disroot.org>
Co-authored-by: Jules <jules@j3s.fr>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Summer-Box1228 <tingrong1228@gmail.com>
2026-05-05 12:43:05 +02:00
Jules Aguillon e57cdc45f2 Revert to the 1.32.1 Russian layout (#1276)
Partly revert be0aa07.
2026-05-05 12:37:39 +02:00
Jon Musselwhite 7b07b14a80 Fix missing null gate (#1273) 2026-05-05 02:26:41 +02:00
Jules Aguillon 063756eb50 Release 2.0.1 (52) 2026-05-03 19:30:18 +02:00
Jules Aguillon 9189921719 Add languages for which a dictionary is available (#1267)
* gen_method_xml.py: Warn for unused dictionaries

Helps catch missing locales.

* Add languages for which a dictionary is available

Make dictionaries are available to more languages. Extra keys are added
for as many languages as possible but no layout is added.

Some languages are still not added, mainly because they use a script
that the keyboard doesn't support yet:

    pa, gu, or, te, mai, sat, km, iw, zgh, sd, ml, sa
2026-05-03 19:19:51 +02:00
Jules Aguillon 2e1859fb1a Fix the keyboard drawing behind the navigation bar (#1265)
The width of the navigation was not accounted correctly.

This reverts be8a99bd
2026-05-03 18:10:43 +02:00
Jules Aguillon 139d1c5691 Fix crash when the cursor moves past an emoji (#1264)
The cursor might move in the middle of a surrogate pair.
2026-05-03 17:09:43 +02:00
Jules Aguillon 20a4916ff6 Fix wrong locale selection for English (#1263)
The "en" locale, which was meant to be the default for English locales
that are not in the list, was used instead of exact locales like "en_US".

The "en" locale was also used as the default locale for languages that
are not in the list at all. The "en_GB" locale is used for this instead.
2026-05-03 16:50:41 +02:00
Jules Aguillon 54c25b79e4 Update dictionaries.xml (#1262)
The dictionary sizes were not correct.
2026-05-03 13:50:51 +02:00
Jules Aguillon 82d61d4406 Fix crash when installing dictionaries (#1261)
This happens in release mode due to minification.
2026-05-03 12:55:27 +02:00
Jules Aguillon e2fdced765 releases: Update languages supported by Google Play (#1257)
Remove languages from fastlane metadata that are not supported by Google
Play and prevented the release from continuing.
2026-05-02 21:34:19 +02:00
Jules Aguillon 78a7ffc473 Release 2.0.0 (51) 2026-05-02 21:11:07 +02:00
+34 467d7e83a5 Merge changes from Weblate (#1256)
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: Packss <enzoana@hotmail.com>
Co-authored-by: A <meow98317@gmail.com>
Co-authored-by: Joppla <joppla@gmx.com>
Co-authored-by: Validbit <validbit@outlook.com>
Co-authored-by: Chasm Solacer <chasmsolacer@users.noreply.hosted.weblate.org>
Co-authored-by: Akal! <ihsani.akmal.0@gmail.com>
Co-authored-by: Edgars Andersons <Edgars+Weblate@gaitenis.id.lv>
Co-authored-by: Mehmet Ali Cuma <v6lhost@proton.me>
Co-authored-by: 是果宝呐 <shiguobaona@163.com>
Co-authored-by: Giuseppe Barichello <beppuz@gmx.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Péter Czeglédy <czegledyp2@gmail.com>
Co-authored-by: Jonatas P <jonatasp5592@gmail.com>
Co-authored-by: Rasel Molla <msg4mrm@gmail.com>
Co-authored-by: Bi Bi Le <lebibibioihandsomeboy.ghost@gmail.com>
Co-authored-by: Juan M Sevilla <jumase@disroot.org>
Co-authored-by: Shine WaiLinn <shinelinn1897@users.noreply.hosted.weblate.org>
Co-authored-by: Sai Hak Moe Htike <pekhejdj@gmail.com>
Co-authored-by: Matteo Borella <borella.matteo@gmail.com>
Co-authored-by: Ulpi Antor <weblate.residual441@passmail.net>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: Yusif Omran <yusifrad20@gmail.com>
Co-authored-by: Samuel Jabez P <jrbros2346@gmail.com>
Co-authored-by: Regu_Miabyss <cyanogenmod.cid@gmail.com>
Co-authored-by: Tannaz M <tannaz.monajemi@gmail.com>
Co-authored-by: winver <kirillstuzhuk@gmail.com>
Co-authored-by: Summer-Box1228 <tingrong1228@gmail.com>
Co-authored-by: E. Ta. <eta00823@gmail.com>
Co-authored-by: Abo Moayd <aboeanas0y2014@gmail.com>
Co-authored-by: meQ Qentlh <brynnhold@gmail.com>
Co-authored-by: Adrià Vilanova Martínez <me@avm99963.com>
Co-authored-by: QonDonion <dolchanat.a@gmail.com>
Co-authored-by: Leonardo Taglialegne <leonardo@taglialegne.it>
Co-authored-by: Mikael Kujanpää <mikael.pajunen@gmail.com>
Co-authored-by: Vaisakh K M <vaisakhkm2625@gmail.com>
Co-authored-by: Debojit Mozumder <debojitmozumder38@gmail.com>
Co-authored-by: Rifat <rifatstudent463@gmail.com>
Co-authored-by: Guzleon <guzleon@yahoo.es>
2026-05-02 20:11:56 +02:00
Jules Aguillon 5d9a105e5d Update the README and screenshots in preparation for the release (#1255)
* README: Acknowledge NLnet

* Move FUNDING.yml into .github/

* Update screenshots

Redo the screenshots with the suggestions visible.
2026-05-02 19:43:23 +02:00
Jules Aguillon 9c23e6c5f3 Emoji suggestion (#1235)
Suggest an emoji in addition to the 3 suggested words when the
current word matches an alias in the emoji dictionary, if
available.
2026-05-02 19:32:21 +02:00
Jules Aguillon d164820bca Reduce the size of suggestions to fit (#1248)
The font size of the suggested words is reduced if the word would
otherwise split on two lines.
2026-05-01 20:25:20 +02:00
Jules Aguillon b844af1f28 Fix the spacebar in Hangul combining (#1244)
* Fix the spacebar in Hangul combining

The encoding for the spacebar changed and the Hangul combining code was
not updated.

* Change the default layout for Korean
2026-04-24 11:32:19 +02:00
Jules Aguillon 6477c90506 CI: Run the tests (#1245)
Run the tests before building the debug APK in CI. This checks that the
unit tests pass and that the generated files are uptodate.

Doesn't fail if the generated files are outdated. Instead, the APK is
built with them updated.
2026-04-18 21:09:55 +02:00
Jules Aguillon c6774196ea Add more symbols to superscript and small_caps (#1198)
* Add more symbols to accent_syperscript
* Add all uppercase and lowercase variants to accent_small_caps
* Use ʁ instead of ᴚ in accent_small_caps
2026-04-18 16:18:13 +02:00
Jules Aguillon af5f6df02c Autocorrect middle word (#1242)
* Fix crash when typing space in the middle of a word

* Don't trigger autocorrect when the cursor is not at the end of a word

This prevented from editing a word that was already typed.

* Don't undo autocorrect at the wrong time

Backspace was trying to undo autocorrect even after some other action
were done after the last autocorrect. This ensures that the keyboard
remembers accurately the last action.
2026-04-16 15:33:28 +02:00
Tony O 08eb3a04a6 Change default locale for en_* and update IME layout (#1237)
Change default unknown locale dictionary from en_US to en_GB
2026-04-14 19:41:53 +02:00
Jules Aguillon e832c3cd80 Fix crash added in #1233 (#1234)
Crash added in https://github.com/Julow/Unexpected-Keyboard/pull/1233
2026-04-12 14:41:28 +02:00
Jules Aguillon a1d0342033 Better spell check with cursor in the middle of a word (#1233)
* Better spell check with cursor in the middle of a word

Suggestions are based on the entire word and no longer the part of the
word just before the cursor. Also, tapping a suggestion or the space bar
autocorrect replaces the entire word.

* Reduce the number of calls to refresh_current_word

While the cursor moves, calls to refresh_current_word are now only done
when the cursor exits the current word.
2026-04-12 14:33:27 +02:00
Jules Aguillon 5ec663c306 Insert a space when undoing autocorrect (#1232)
This prevents the same suggestions to be presented again and autocorrect
to be activated again when pressing space.

This extra space is wanted because pressing the spacebar triggered
autocorrect in the first place.
2026-04-12 14:30:04 +02:00
Jules Aguillon 833b4a2154 Better suggestion with diacritics (#1223)
* Update cdict

* scripts/subst_of_compose.py: Compute substitutions

from compose mappings. They are used when building dictionaries.

* Add substitutions compose data

* Better suggestion with diacritics

This improves the suggestions for words that contain diacritics and
uppercase letters.

This works by stripping diacritics both when building the dictionaries
(using word aliases added in cdict: https://github.com/Julow/cdict/pull/3)
and during lookup. Cdict then takes care of resolving the correct word.

The substitutions are generated using mappings from `fn`, `shift` and
all the `accent_*` modifiers into srcs/compose/substitutions.json
This can be updated easily when more mappings are added.
2026-04-10 19:08:23 +02:00
Jules Aguillon 8921de7f02 Handle Google Keep bug (#1230)
The Google Keep text area sends contradictory flags to keyboards,
resulting in the suggestions to be disabled.
This seems to be a bug in Google Keep.
2026-04-10 19:04:03 +02:00
Jules Aguillon afa7c0187a Refresh suggestions immediately after language switch (#1229)
* Switch dictionary when language changes

The dictionary was otherwise updated by closing and opening the keyboard
again.

* Refresh suggestions immediately after language switch
2026-04-10 19:03:51 +02:00
Jules Aguillon 7bd9306a9f Dictionary activity: Status message when the keyboard is not enabled (#1228)
This shows a reason why the list is empty and removes confusion.
2026-04-10 19:03:37 +02:00
Jules Aguillon f29ae92ba7 Disable spacebar autocomplete when suggestions are hidden (#1227)
The spacebar autocomplete option was still working even when the
suggestions strip was correctly hidden in applications like Termux.
2026-04-10 19:03:20 +02:00
Jules Aguillon 5f63214c08 Increase lookback when looking for the last word (#1226)
For example, typing "dictionarirs" suggests "dictionaries" but moving
the cursor away and back results in no suggestion. Because the lookback
was 10 characters long, the keyboard was seeing "ctionarirs".
2026-04-10 15:18:50 +02:00
Spike 0f3ec187c9 Documentation: Point to default mappings as requested in #1222 (#1225) 2026-04-10 14:46:29 +02:00
Jure Varlec 13f1996af9 Fix building on NixOS (#1219)
* shell.nix: bump platform version to match target

* shell.nix: make gradlew work on NixOS

This removes the wrapper provided by `shell.nix`. Folks expect the
`./gradlew` wrapper to work, so this approach is more straightforward.
2026-04-08 17:38:45 +02:00
Hopeine 35ccaafe65 doc: Fix hyperlink of bottomw_row.xml and number_row.xml (#1216)
Add blob/master/
2026-04-08 17:29:34 +02:00
Tony O 963399417e Add Finnish QWERTY keyboard layout XML (#1221) 2026-04-05 23:34:36 +02:00
Jules Aguillon 4dab3a609c CI: Check whether ComposeKeyData is uptodate (#1204) 2026-03-16 00:00:41 +01:00
Jules Aguillon 2966a45922 CI: Disable workflow runs on push (#1202) 2026-03-15 01:34:19 +01:00
Tyler b25b670c6b Making dictionaries compatible with 16 KB devices. (#1199) 2026-03-15 01:27:37 +01:00
Jules Aguillon ece8fb51ce Add Assamese layout (#1196) 2026-03-06 13:45:01 +01:00
dzaima 18157a1cd8 Make layout text editor not redraw every frame (#1192) 2026-03-06 13:44:48 +01:00
dzaima 27fdcb6bfc Some slider changes (#1195)
* Helper for whether a slider is vertical

* Only accumulate delta for the current slider's direction

* Slow down horizontal cursor movement while ctrl is held
2026-02-27 00:41:48 +01:00
dzaima 6a19f7ccd3 Separate colors for pressed vs activated labels (#1193) 2026-02-25 23:57:39 +01:00
dzaima 164c8732c0 Add missing alphabet + ~ compose sequences (#1194) 2026-02-25 23:55:31 +01:00
Jules Aguillon 958900ca2f Update target SDK version to 36 (#1190)
* Fix keyboard not showing on Android 16

* Update target SDK version to 36
2026-02-25 00:12:43 +01:00
Jules Aguillon ae4a9d4514 Fix no keyboard after switching from clipboard manager (#1189) 2026-02-24 23:57:10 +01:00
Jules Aguillon 91b2970c1f Fix crash introduced in #1183 (#1187) 2026-02-24 22:50:28 +01:00
Jules Aguillon 9863dcea9e gradle: Show an error when Git submodules are not initialized (#1188)
* CONTRIBUTING: Document Git submodules

* build: Error during build if the submodules are not initialized
2026-02-24 22:50:16 +01:00
Jules Aguillon a80afa5fbe Improve suggestion for capitalized words (#1183)
Suggestions were previously case-sensitive, which gave very bad results
for the first word of a sentence.

This makes sure that the suggestions for capitalized words are as good
as for all lower-case words and that the suggestion doesn't turn the
uppercase letter into lower case.
2026-02-22 21:13:11 +01:00
Jules Aguillon d172455eb4 Fix space and delete not repeating (#1182) 2026-02-21 13:08:42 +01:00
Jules Aguillon df3594bd53 Autocomplete on space bar and undo on delete (#1179)
* Refactor: Implement the space key as an editing action

The space key is no longer a CHAR key but now an EDITING key. This
allows changing the behavior of the space bar while allowing custom
layout users to define a key outputing a space as before.

KeyModifier and ComposeKey are updated to treat the space key as before.

* Enter the best suggestion when the space bar is pressed

* Refactor: Implement backspace as an editing action

* Undo suggestion when delete is pressed

The delete key (backspace internally) undoes the suggestion if the last
action done was entering the suggestion with either the space bar or the
candidates view.

* Add an option to enable space bar autocomplete

This option is off by default. Undoing a completion with backspace works
in any case.

* Refactor: Track selection emptyness in CurrentlyTypedWord

This removes an expensive RPC call.
2026-02-19 00:37:42 +01:00
Jules Aguillon ebf80415d8 Refactor: KeyValue constants (#1180)
* Refactor: KeyValue constants

Add constants for keys that are accessed from the app's code. This
simplifies the fake pointer handling in Keyboard2View.

* Refactor: Pre-compute action key replacement

The action key and the enter swap are pre-computed in EditorConfig. This
simplifies the code.
2026-02-19 00:35:14 +01:00
Jules Aguillon 1cd252f8fa Increase maximum value for the bottom margin option (#1178) 2026-02-14 16:04:30 +01:00
Jules Aguillon 0109f30e22 Fix label color when keys are pressed (#1173)
The color of labels on a pressed key that were not the label being
activated (eg. the labels on the corners when activating the label in
the centre) was not correct.
2026-02-08 00:33:52 +01:00
Jules Aguillon 77c4a27c4c Spell checking (#1137)
This adds dictionary-based spell checking to the keyboard. The keyboard looks at the word being typed and matches it against a dictionary to either complete the rest of the word or find alternative spellings.

The core of this feature is implemented in cdict, which is included as a
submodule in vendor/cidct.

Cdict is developped at https://github.com/Julow/cdict
The dictionaries are hosted at https://github.com/Julow/Unexpected-Keyboard-dictionaries/
The wordlists used to build the dictionaries are the same ones used by
HeliBoard from https://codeberg.org/Helium314/aosp-dictionaries

- Add an activity accessible from the launcher app that lists available
  dictionaries with a download button.
  The DictionaryListView view shows the list of available dictionaries and
  handles downloading and installing them.

- The Dictionaries class manages installed dictionaries. Dictionaries are
  installed as individual files into the app's private directory.

- Available dictionaries are listed in dictionaries.xml, which is generated
  when building Unexpected-Keyboard-dictionaries.
  method.xml mentions the dictionary name for each locales.
2026-02-02 00:20:00 +01:00
Jules Aguillon 2ecf93d990 Candidates view improvements (#1168)
* Refactor: Create subpackage 'suggestions'

* Candidates view: Status message when no dictionary installed

Show a message on the candidates view instead of leaving it empty. A
button points to the dictionary installation activity.

* Add an option to disable the suggestions

* Refactor: Remove Config.should_show_candidates_view

This was moved to EditorConfig.

* Don't disable text suggestions in some text boxes

* Suggestion text size matching settings

The candidates view height is based on the height of keyboard rows and
the suggestion text size is based on the size of labels on the keys.

This is influenced by symbol size and keyboard height options.
2026-02-01 23:25:38 +01:00
Jules Aguillon b9072daaf6 Avoid loops in modmaps (#1167)
It was possible to define a loop that would crash the app with the
following modmap:

    <ctrl a="left" b="left:ctrl,left"/>

With this change, modmap are not taken into account while evaluating
macros. The modmap above modifies ctrl+left into ctrl+left.
2026-01-31 15:36:22 +01:00
Jules Aguillon 1b8506876a Option to switch to the previously used keyboard (#1165)
The "Switch to the last used keyboard" is renamed to "Switching between
input methods" and allows to select "Switch to last used", which was the
default in older releases.
2026-01-30 21:18:48 +01:00
Jules Aguillon e05be0821b README: Remove the "Similar apps" section (#1163) 2026-01-25 13:47:44 +01:00
Jules Aguillon c669a755a6 Add the small capital letters dead key (#1156)
* Add the small capital letters dead key

It can be added to the keyboard from the settings or into custom layouts
as `accent_small_caps`.

Add all the symbols listed on https://en.wikipedia.org/wiki/Small_caps
2026-01-18 21:57:17 +01:00
Jules Aguillon 3b4aa33ff3 Fix crash when using the Monet themes on Android 9 (#1155)
The color resources starting with `@android:color/system_` are not
available on API < 31.

No effort is made to implement the Monet feature on Android 9. Instead,
the monet themes look like the Dark and Light themes.
2026-01-18 17:28:10 +01:00
Jules Aguillon a848897586 Refactor: Split DeviceLocales out of Keyboard2 (#1154)
* Refactor: Split DeviceLocales out of Keyboard2

This moves the code from Keyboard2 that handles the active IME subtypes
and the current one.

The goal is to access this information from the dictionary activity
while simplifying the code of Keyboard2.

* Refresh the candidates view when subtype changes

This fixes the status message being inconsistently shown.

* Refactor: Load DeviceLocales less often

Previously, [DeviceLocales.load()] was called everytime the keyboard was
shown on the screen. This operation is moderately costly and only need
to be done when the IME subtype changes.

* EditorConfig: Fix crash on Android 9
2026-01-18 17:26:41 +01:00
Jules Aguillon 4d4c22b4e2 Merge pull request #1150 from Julow/candidates_view
Candidates view
2025-12-28 19:02:45 +01:00
Jules Aguillon bdb05bcef0 Fix CurrentlyTypedWord counting the first letter twice 2025-12-28 18:13:17 +01:00
Jules Aguillon 2208079e48 CurrentlyTypedWord: Handle key events
Currently, refreshing the current word on each key event.
Refreshing is done after a short delay, to ensure the editor has handled
the event.
2025-12-28 18:13:17 +01:00
Jules Aguillon c4d802d873 CurrentlyTypedWord: Handle selection
When selection is entered, the current word is cleared.
2025-12-28 18:13:16 +01:00
Jules Aguillon 2b60f94ead Enter the suggestion when it's pressed
The current word is replaced by the pressed suggestion.
2025-12-28 18:08:36 +01:00
Jules Aguillon f082fcdebc Track the currently typed word
The `CurrentlyTypedWord` class tracks the word that is being typed. It's
implemented on the same model as Autocapitalisation and avoid expensive
IPC calls when possible.

The `Suggestions` class is where the suggestion lookup should go. It
currently just echoes the current word.
2025-12-28 18:08:12 +01:00
Jules Aguillon 98c1b8db82 Candidates view
The `CandidatesView` is implemented as a `LinearLayout` that is divided
horizontally with up to 3 `TextView`. It might in the future contain
buttons on the sides.

The candidate view is nested into the input view rather than using
Android's `setCandidatesView` and callbacks as the API is unreliable and
complicated.

The first suggestion goes in the middle to be more accessible. The
second suggestion goes on the right to be more accessible to the
right-handed, because it must go somewhere.
2025-12-28 17:56:37 +01:00
Jules Aguillon dfaf4dbb57 More practical definition of method.xml (#1149)
Locale are now defined in `gen_method_xml.py` in a more convenient and
checked way. It contains both the locale definition and the generation
script.

`method.xml` is generated with:

    gradle test

The goal is to support dictionaries for spell checking.
2025-12-27 23:49:50 +01:00
Jules Aguillon dab50e0572 Fix clipboard manager crashes (#1148)
* Fix background crash in clipboard history service

The clipboard history service might crash when the connection between
the keyboard and the system is in a bad state.

* Fix race condition in clipboard history service

The clipboard history service callback appears to be called concurrently
leading to concurrent accesses to data and to concurrent calls to
'on_clipboard_history_change'. Access are now synchronized.

* Fix clipboard manager crash when device is locked
2025-12-26 16:43:34 +01:00
Jules Aguillon b40be68773 Fix various linting issues (#1146) 2025-12-22 01:27:07 +01:00
Jules Aguillon 258215f26c gradle: Remove unecessary rule
The output APKs used to be named 'juloo.keyboard2.apk' are now named
'Unexpected-Keyboard-release.apk' since the Gradle upgrade and the rule
setting it hasn't been working since.
2025-12-18 19:50:22 +01:00
Jules Aguillon 41777fdda6 Disable selection mode in text editors (#1141)
* Disable selection mode in text editors

Selection mode removes the space bar key (which is replaced by the Esc
key) and can be annoying in Emacs for example.
Text editor users probably have the `esc` key available.

* Refactor: Move EditorInfo related code to EditorConfig

Add the new EditorConfig class that handles configuration extracted from
the EditorInfo. It is accessible from the Config class for convenience.

This aims at reducing the length of already large classes and group the
code that was spread over several classes.
2025-12-18 19:15:32 +01:00
10Develops be0aa07a2a Added keyboards (#1129)
* Delete srcs/layouts/armenian_ph_am.xml

There is a phonetic layout which is used in old typewriters

* Amendments according to GOST 6431-90
* Russian traditional layout cyrl_jiuken.xml
* Create cyrl_yawerty.xml

Added ЯВЕРТЫ layout

* Update cyrl_jcuken_ru.xml

Typo fixed

* Added QZERTY layout for Italian and Latin
2025-12-13 23:05:29 +01:00
Jules Aguillon 5fc1da0a2e Remove emoji history migration function (#1139)
This function was used to migrate the representation of the emoji
history and needed a very large table.

This reduces the app size by 22KB.
2025-12-13 22:12:36 +01:00
Jules Aguillon f834b2a82a AZERTY: Make sure the accents are available (#1140)
Remove the `loc` prefix for important accent dead keys and add ê.
2025-12-13 22:09:57 +01:00
Jules Aguillon 498194231f Update the emoji list to Unicode 17.0 (#1138) 2025-12-13 21:40:46 +01:00
Jules Aguillon 08ebf8fabc Fix parsing of escaped characters in macros (#1126)
* refactor: Better printing for KeyValue in tests

* Fix parsing of escaped characters in macros

The parsing code was bugged with custom macros like `symbol:\abc` and
`symbol:\\abc`.
2025-12-13 15:58:34 +01:00
Andy Callisto 0ad03c6dea Ukrainian l10n update (#1128)
* Ukrainian l10n update

* "Debug" translated
2025-11-19 21:00:21 +01:00
dzaima be8a99bdc6 Fix insets being excluded from computed width (#1127) 2025-11-10 18:32:41 +01:00
Jules Aguillon 00cf534e00 build: Avoid unreproducible rules in regular builds (#1125)
The genEmojis rule makes a network request and must not be done during a
regular build.
The compileComposeSequences updates a file that is checked in the
repository and doesn't need to be updated in a regular build. Both are
now handled like 'genLayoutsList'.
2025-11-10 16:27:29 +01:00
Autissima c717461ef5 Turkish F layout (#1096) 2025-11-01 23:47:35 +01:00
Jules Aguillon eae05bb9bd gradle: Set explicit dependency for rule genLayoutsList 2025-11-01 23:43:03 +01:00
Jules Aguillon ac498d4ee9 Update ComposeKeyData.java
Missing from previous commit.
2025-11-01 23:24:34 +01:00
Houfalafel 02c11132b2 Update extra.json (#1118)
change compose for n͠g to g + n + ~ to avoid overlap. this in order to add n + g = ŋ. also added lowercase Ğ.
2025-11-01 23:23:22 +01:00
wei lang 635c7d5882 Cobalt & Pine theme (#1084) 2025-09-29 01:03:35 +02:00
Matej Drobnič df8e7a01e0 Make history duration configurable (#1074) 2025-09-29 00:56:23 +02:00
J. Fronny 7fdf63b930 Upgrade Gradle (#1097)
* Enable configuration cache and up-to-date checking
2025-09-29 00:37:26 +02:00
Spike 462b860e4e Custom-layouts.md: Document minimum row height (#1101) 2025-09-15 14:16:26 +02:00
Jules Aguillon 8db00d51ab Allow smaller row height in custom layouts (#1100)
The height of row set with `<row height="...">` cannot be lower than 0.5
to avoid rendering problems.
This restriction is not necessary when the row has no key and is lifted
in this case.
2025-09-15 14:15:55 +02:00
Jules Aguillon ab85fb717d shell.nix: Remove repoJson
nixpkgs-unstable is now uptodate.
2025-09-04 00:25:56 +02:00
Autissima 5b3dc8d5ed Python script encoding fix (#1093)
* Changed Python scripts so they work on my machine

file open() used default encoding (cp1252 on my machine) but the text is utf-8, had to specify encoding in check_layout.py
2025-09-03 22:41:10 +02:00
Jules Aguillon 795889bf81 README: Remove capitals in "Open Source" 2025-09-01 00:02:02 +02:00
Jules Aguillon 6d0aa4cf51 Use the <fn /> mappings first when typing a gesture (#1085)
This allows assigning mappings to letters without changing the Shift
mappings and making uppercase impossible to type.

For gestures, the Shift modifier is still applied first, unless a custom
mapping for <fn/> is set for the key.
2025-08-31 23:35:34 +02:00
Animesh dfb2c7eaf9 add Everforest Light theme (#1080) 2025-08-24 22:36:45 +02:00
Jules Aguillon cedf3aeb1c Slider sensitivity option and improve slider ergonimics (#1070)
* Add option 'Space bar slider sensitivity'

* Fix slider moving the cursor twice at the beginning

The slider was generating two events when first activated.
Since 2bed42857.

* Make slider less sensitive for the first step

This moves the second slider step approximately 40% further and takes the
new sensitivity option into account.
Allows moving the cursor by one spot more easily.
2025-08-17 19:24:08 +02:00
Jules Aguillon c1d473b4a0 Add support for four important Nigerian languages (#1077)
This adds the English, Hausa, Yoruba and Igbo languages for Nigeria. The
goal is to add the ₦ symbol as well as basic support for these
languages. No layout is added.
2025-08-17 01:59:46 +02:00
Jules Aguillon 00d7d88271 gradle: Disable sync_translations (#1076)
The diffs created by sync_translations.py creates conflicts with the
changes made on Weblate. It is disable to make merging translations from
Weblate easier.
2025-08-16 18:55:26 +02:00
dom 7e4e4bf4d9 Update Custom-layouts.md reference to web editor (#1064) 2025-08-07 20:15:55 +02:00
Jules Aguillon 020e80019b Fix very small label size on 11+ columns layouts (#1046)
The label size was computed against the width of keys on the keyboard,
which resulted in a different label size on different layouts.
Notably, it made the labels very small on a 11 columns layout and
relatively larger on the numpad.

The label size is now computed against a baseline of 1/10 of the width
of the screen.
2025-08-07 20:07:59 +02:00
Jules Aguillon 679f52cf45 Add '⊢' to the greekmath layout 2025-08-07 12:25:42 +02:00
cillyvms 9588f55619 Fix alternate keyboard layouts rendering behind navbar on some devices. (#1062) 2025-07-30 22:47:37 +02:00
Skynetcat 5c7484388f doc: Added Arabic diacritics to documentation of possible key values (#1061) 2025-07-26 12:35:24 +02:00
Jules Aguillon 2bed42857b Fix missing swipe vibration and visual feedback (#1054)
This was removed by mistake in 42528ab211.
2025-07-22 22:03:53 +02:00
Harry-Ais0 fad74bb123 Add compose sequences for ꜩ Ꜩ (#1058) 2025-07-22 22:02:55 +02:00
Anastázius Kaejatídarján 43dd3a2818 Added Majuscules to the Cyrillic Compose Sequences (#1042)
* add majuscules to compose
* add tse to compose
* added reverse ya FN to modernize it FROM the older form
* fix accent FN and add reverse arabic-heh FN
* little improvements and additions
* QoL improvements for Kurdish Sorani
2025-07-19 17:54:57 +02:00
JeelsBoobz c4a1e2e664 Fix autocapitalisation flicker (#1047) 2025-07-19 17:50:27 +02:00
Mal Bajun 13acf90541 Replaced combining_acute with combining_aigu (#1040) 2025-07-13 19:51:55 +02:00
Matej Drobnič a83b40066e Use screen width to switch layouts (#1029)
This changes layout switching logic to detect screen width rather than increasingly specific portrait/landscape/unfolded state.

This reduces number of different auto-layouts back to 2: narrow devices (such as a phone in portrait) and wide devices (such as a phone in landscape or a tablet).
2025-07-11 19:14:36 +02:00
Mal Bajun c6e35ba4b6 Improve layout cyrl_jcuken_as (#1039) 2025-07-11 18:55:20 +02:00
Jules Aguillon e013128142 Release 1.32.1 (50)
Check layouts / Generated files (push) Has been cancelled
Check layouts / check_layout.output (push) Has been cancelled
Make Apk CI / Build-Apk (push) Has been cancelled
2025-07-09 00:24:29 +02:00
Jules Aguillon 898956256a Workaround Godot editor not implementing setSelection() (#1033)
This was tested against:

    org.godotengine.editor.v3
    org.godotengine.editor.v4
2025-07-06 23:18:58 +02:00
Jules Aguillon 8cb9b30461 Fix crash with unexpected value in NumberLayout pref (#1036) 2025-07-06 21:57:41 +02:00
solokot 309fba4766 Update Russian translation (#1035) 2025-07-05 12:45:51 +02:00
Jules Aguillon d1dfc80df6 ci: Disable translation check
The translation check is creating conflicts with changes done on Weblate
and is superfluous.
2025-07-02 23:15:33 +02:00
Joey Schaff 5c4964e438 Add a missing comma in /res/values/strings.xml (#1026) 2025-07-02 23:13:12 +02:00
Jules Aguillon 957dbdc7fe Fix fastlane metadata 2025-07-02 01:52:43 +02:00
Jules Aguillon babc3ec88a Release 1.32.0 (49)
Check layouts / Generated files (push) Has been cancelled
Check layouts / check_layout.output (push) Has been cancelled
Check translations / check-translations (push) Has been cancelled
Make Apk CI / Build-Apk (push) Has been cancelled
2025-07-02 01:05:52 +02:00
Jules Aguillon 42fa47459c Promote ComposeKeyData.java 2025-07-02 00:30:58 +02:00
Jules Aguillon 30d1f5635c Update French translation 2025-07-02 00:30:50 +02:00
+9 75c1ce38df Pull translations from Weblate (#1025)
Translated using Weblate

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: Packss <enzoana@hotmail.com>
Co-authored-by: A <meow98317@gmail.com>
Co-authored-by: Joppla <joppla@gmx.com>
Co-authored-by: Validbit <validbit@outlook.com>
Co-authored-by: Chasm Solacer <chasmsolacer@users.noreply.hosted.weblate.org>
Co-authored-by: Akal! <ihsani.akmal.0@gmail.com>
Co-authored-by: Edgars Andersons <Edgars+Weblate@gaitenis.id.lv>
Co-authored-by: Mehmet Ali Cuma <v6lhost@proton.me>
Co-authored-by: 是果宝呐 <shiguobaona@163.com>
Co-authored-by: Giuseppe Barichello <beppuz@gmx.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Péter Czeglédy <czegledyp2@gmail.com>
Co-authored-by: Jonatas P <jonatasp5592@gmail.com>
2025-07-02 00:28:53 +02:00
Mal BajunandPavel «Mal Bajun» Skrylev c308b57611 feat: added јcuken leyout for all slavic language (#992)
Co-authored-by: Pavel «Mal Bajun» Skrylev <majioa@yandex.ru>
2025-07-01 23:56:07 +02:00
Jules Aguillon 52c0e8c798 Add cyrillic combining kavyka and palatalization (#998) 2025-07-01 23:42:49 +02:00
Jules Aguillon 1ff3bf2fc9 Make vertical sliders slower (#1023)
Make vertical sliders slower. The intention is to make the up/down
slider slower, as we have less visibility and do smaller movements in
that direction.
2025-07-01 23:41:49 +02:00
Jules Aguillon a08db25661 Fix extra bottom margin when navbar buttons absent (#1024)
* Fix extra bottom margin when navbar buttons absent

Fix the extra space that was appearing when the gesture-navigation bar
didn't contain the "switch IME" or "close IME" buttons.
This was found on OneUI 7 with the two "keyboard key" related options
turned off.

* Remove unneeded nav bar detection and width computation
2025-07-01 23:27:24 +02:00
Matej Drobnič 4ed8594794 Add option to also disable number entry layout (#1007) 2025-06-29 16:50:56 +02:00
Jules Aguillon f36a7c8e9d Fix unwanted bottom margin on Samsung OneUI 7 (#1022)
This fixes the unwanted margin at the bottom of the screen on
Samsung One UI 7 based on Android 15.
2025-06-29 16:18:49 +02:00
solokot 004ed95dfd Update Russian translation (#1021) 2025-06-29 11:41:12 +02:00
alotbsol555 9d12981b34 cut and copy with built in keys only if text is selected (#1019)
cut and copy with built in keys only if text is selected to keep existing clipboard content. does not work for key combinations like ctrl+x or ctrl+c.
2025-06-21 22:15:22 +02:00
Jules Aguillon c6da6d6ab5 Fix selection mode not exited when selection cleared (#1020)
The selection mode was not exited when the selection was cleared with,
for example, ctrl+x.
2025-06-19 23:12:37 +02:00
David 2e0be0b32b Added Macedonian language layout (#1016) 2025-06-19 23:11:17 +02:00
Jules Aguillon 42528ab211 Improve selection mode when selection ends cross (#1013)
Emptying the selection with the spacebar slider puts the selection mode
in an unexpected state. This checks that this doesn't happen.

The selection ends can now cross each other, reversing the selection
order. Special care is made to make sure that the sliders go in the
expected direction.

Pointers is changed to send a `onPointerDown` for the first slider event
instead of a `onPointerHold`. This event is detected in
`KeyEventHandler` and the selection ends are reordered if needed when
sliding again.
2025-06-19 01:10:06 +02:00
Jules Aguillon 71d2784026 Add <row scale=""> attribute (#1018)
* Add <row scale=""> attribute

Scale the width of the keys in the row to match a value. Useful to
remove space on the right of the row without adding a 'width' attribute
to each key.

* layouts: Use the 'scale=""' attribute in builtin layouts

This removes the very specific 'width' values.
2025-06-18 00:49:34 +02:00
Jules Aguillon f9b844e988 Increase the clipboard history size to 6 2025-06-14 11:12:21 +02:00
Jules Aguillon 6f0244a29e Add funding.json 2025-06-04 02:08:31 +02:00
Jules Aguillon 2a50a4a129 Fix high keyboard height making it overflows
The calculation for the size of each rows now avoid making the keyboard
bigger than the screen if the Keyboard Height option is unusually high.

The height calculation with the default settings is changed slightly, it
is now assuming that a layout is 3.95 rows high instead of 4. This is
because the bottom row is usually 0.95% the size of other rows.
2025-05-30 20:07:26 +02:00
Jules Aguillon 9ab099175d Fix label size when the keyboard is unusually high
The label size is computed relatively to the width of the keyboard when
the height is larger than 3/2 of the width. This ensures that the labels
have a reasonable size when the keyboard height increases.
2025-05-30 20:07:26 +02:00
Jules Aguillon 0445c310ad Settings: Remove the upper limit in keyboard height
This can be used on phones that have several screens.
2025-05-30 20:07:26 +02:00
tenextractor 536b8d1f67 Add icelandic layout (#999) 2025-05-25 16:44:50 +02:00
Jore d36452d9c3 Update layout customization info in srcs/layouts/latn_qwerty_us.xml (#1001)
- Added info on bottom row customization. (#637)
- Added useful links.
2025-05-25 16:40:15 +02:00
Matej Drobnič 97355881a8 Better support for foldable devices (#982)
* Add AndroidX WindowManager

unfortunately, this seems to be the only way to get fold state, native Android APIs are internal. To add this, we need to update some dependencies, raise java version and raise compile SDK.

* adds separate layouts and separate layout settings for folded and unfolded state of the device.

The affected settings are:
+ the margin bottom settings
+ the horizontal margin settings
+ the keyboard height settings

* Update shell.nix
2025-05-22 23:54:13 +02:00
Jules Aguillon a7312054b5 compose: Add compose+A+e as compose+A+E and many others
compile.py is changed to report when compose+Upper+Upper exists but
compse+Upper+Lower do not.

With these findings, many sequences are added.
2025-05-16 00:59:35 +02:00
Jules Aguillon 3b94bf6a17 Fix duplicated or missing keys and more in layouts
Fix several warnings spotted by check_layout.
2025-05-11 23:06:19 +02:00
Jules Aguillon ff9d2e7d31 check_layout: Check for unknown keys
This can spot mispelled special keys.
2025-05-11 23:05:57 +02:00
Spike b670fe0da2 Clarify use of \\ per Julow in Julow#989 (#990) 2025-05-08 23:04:45 +02:00
solokot 32e82d6791 Update Russian translation (#986) 2025-04-27 00:16:45 +02:00
Anton Karmanov de5c5b5b60 Sinhala phonetic layout (#965) 2025-04-20 20:05:02 +02:00
Jules Aguillon d6dfb5c18c Add compose sequences for n͠g, N͠g and g̃ 2025-04-19 15:10:07 +02:00
Jules Aguillon 460e5214a2 Add class Modmap and test KeyModifier 2025-04-15 00:05:52 +02:00
Jules Aguillon 5ae3ec05e7 strings: Improve "remove clipboard item" prompt 2025-04-14 22:44:37 +02:00
Jules Aguillon 70fb1e8fc1 strings: Shorter name for the settings activity 2025-04-14 22:39:42 +02:00
Jules Aguillon 2bd3e107ac Remove the Share key from preferences 2025-04-14 22:32:38 +02:00
Jules Aguillon 4ae45bd00f Add ù and û to French languages 2025-04-14 22:29:52 +02:00
Jules Aguillon 7a6aef3360 launcher: Fix animations overlapping 2025-04-12 23:30:22 +02:00
Jules Aguillon 9fac081ab8 launcher: Remove fade-in in animations
The fade-in caused the trace to be hidden when animations are disabled.
2025-04-12 23:25:41 +02:00
solokot b4e8b10e2b Update Russian translation (#976) 2025-04-06 22:49:26 +02:00
Jules Aguillon a7d73ebd8a Release 1.31.1 (48)
Check layouts / Generated files (push) Has been cancelled
Check layouts / check_layout.output (push) Has been cancelled
Check translations / check-translations (push) Has been cancelled
Make Apk CI / Build-Apk (push) Has been cancelled
2025-03-29 15:09:38 +01:00
Jules Aguillon 0acf66d455 Restore previous swipe sensibility
The distance at which a swipe was considered successful was reduced in
188c682 to allow making circle gestures on a key that also had a slider.
It was also useful in reducing accidental sliding.

This is impractical for normal typing so it is reverted. The reduced
range continues to be used for sliders.
2025-03-29 12:04:36 +01:00
Jules Aguillon 85c73d9f62 CONTRIBUTING.md: Update section on srcs/compose 2025-03-29 11:21:50 +01:00
Jules Aguillon 58dabfaa07 Fix emoji flag group missing
Off by one error when parsing the emoji groups.
2025-03-28 00:27:44 +01:00
Haley Halcyon 3d7925340b Update latn_qwerty_jp.xml (#962)
Add macron, used in rōmaji for long vowels
2025-03-28 00:04:31 +01:00
Jules Aguillon e746ccd5d0 Place editing keys on the bottom-right of Z/X/C/V
The code is changed to ensure that extra keys all use a consistent
location.
2025-03-27 23:49:49 +01:00
Jules AguillonandJoey Schaff 6541054cce Remove symbols from the number row by default (#964)
The number row option is changed into a ListPreference and controls whether the number row contains symbols or not.

Co-authored-by: Joey Schaff <j@jaoh.xyz>
2025-03-27 23:25:32 +01:00
Spike 3971f6243c Possible-key-values.md—Note syntax change for slider (#958)
The non-backward-compatible change to the XML should be mentioned as users must revise their layouts.
2025-03-27 22:50:34 +01:00
Jules Aguillon 6c40e0604a Add a preferred position for selectAll 2025-03-23 14:58:11 +01:00
Jules Aguillon 1d6a115ce4 Add link to Weblate on the README 2025-03-22 16:32:57 +01:00
Jules Aguillon 6717613b5e Fix label brightness not being applied
This is due to Paint.setColor overriding any alpha value set with
Paint.setAlpha. Bug was added in 653c598.
2025-03-22 12:25:59 +01:00
Joey Schaff 35219a458a Swapping the default placement of ! and | on the Colemak layout (#949)
Swapped placement of ! and | to make it more reasonable.
2025-03-22 11:46:52 +01:00
Jules Aguillon f0ebe45439 Release 1.31.0 (47)
Check layouts / Generated files (push) Has been cancelled
Check layouts / check_layout.output (push) Has been cancelled
Check translations / check-translations (push) Has been cancelled
Make Apk CI / Build-Apk (push) Has been cancelled
2025-03-16 15:45:05 +01:00
Jules Aguillon 0ef36d52e1 Fix crash on API < 26 in settings 2025-03-16 15:44:02 +01:00
0skar2 08860eed17 Better material themes (#904) 2025-03-16 14:59:41 +01:00
Jules Aguillon b8bd3f7b0f Pull changes from Weblate 2025-03-16 14:47:59 +01:00
Jules Aguillon 60b01927ab Selection mode (#913)
* Selection mode: Space to cancel the selection

This adds the "selection mode", which is activated when text is selected
in the text box. The selection mode is exited when the selection is
cleared.

While the selection mode is activated, the Space and Esc keys are
modified into the "selection cancel" key, which remove the selection
without changing the text.
The space bar is otherwise easy to type by accident during a selection
and causes the selected text to be deleted.

* Selection mode: Move each ends of selection separately with slider

When the selection mode is activated, the space bar sliders change how
they affect the selection:

- The left side of the slider moves the left position of the selection.
  To shrink the selection from the left side, the slider must be
  activated by sliding to the left, extending the selection
  temporarilly, then by sliding to the right.

- The right side of the slider affects the right position if the
  selection.
2025-03-15 16:13:16 +01:00
Jules Aguillon 9cfeb0f0c2 Add a delay after a Keyevent key in a macro (#918)
* Construct a single handler

* Add a delay after a Keyevent key in a macro

Add a delay before sending the next key to avoid race conditions causing
keys to be handled in the wrong order. Notably, KeyEvent keys handling
is scheduled differently than the other edit functions.
2025-03-10 23:41:51 +01:00
Spike 5e77fa84cf doc: Massage section on "Escape codes" (#912)
* Add introductory text to the tables

And mention characters that don't have escapes

* Turn tables around

Tables should be structured by what the user wants, not by what the code does.

* Address Julow review #1

- Merge tables, no matter which rule requires escaping; "in the usual way for XML" applies to both
- 3 escapes not mandatory removed from table to new ¶ below
- Found one more symbol → legend

* doc: Clarify escaping of comma and colon per #915
2025-03-08 12:11:54 +01:00
Jules Aguillon 06fbc83c9c sync_translations.py: Handle '\n' in store descriptions
These are added by Weblate.
2025-03-08 12:08:57 +01:00
Jules Aguillon a123810247 Change indentation of strings.xml files to match Weblate
This runs sync_translations.py, which also remove uneeded comments.
2025-03-08 12:05:35 +01:00
Jules Aguillon 906755b787 prefs: Add 'delete_word' and 'forward_delete_word' to extra keys
The gesture combination is mentioned. Preferred position are added.
2025-03-01 16:52:46 +01:00
Jules Aguillon 80c52460c7 Add 'delete_word' and 'forward_delete_word' keys
These keys are the equivalent of ctrl+backspace and ctrl+delete,
respectively.

They can be reached with Gesture+backspace and Gesture+delete
respectively.
2025-03-01 16:52:46 +01:00
dzaima 2b978dd2e0 APL and BQN layouts (#899)
* Add compact BQN layout

* Add compact APL layout
2025-02-28 00:47:49 +01:00
Jules Aguillon 55cb5f5315 Make key symbol smaller when 2 characters long or more
This was the case for string keys but not for macros or keys with custom
symbols.
2025-02-28 00:36:14 +01:00
Jules Aguillon b7d1508d7b Refactor: Remove StringWithSymbol
It's replaced with a macro containing one key.
2025-02-28 00:17:28 +01:00
Jules Aguillon 77b0c30728 cyrl_lynyertz_sr: Use the newer key syntax for ctrl mappings 2025-02-27 23:56:26 +01:00
Jules Aguillon 466e0b8218 KeyValue: Don't wrap keys into a macro when possible
Many kind of KeyValues don't need to be wrapped into a Macro to show a
specific symbol. This is especially useful as Macro keys are not
affected by modifiers.
The parser is changed to have a fast-path when a key value is not a
macro.
2025-02-27 23:53:42 +01:00
Jules Aguillon 75fdc2bfa9 Fix miscalculation of the bottom margin
'_marginBottom' might be uninitialized when used.
2025-02-27 23:20:49 +01:00
Jules Aguillon bee59aceaa Show a message in voice input chooser when empty
This avoids creating an empty dialog that appears as a mysterious white
rectangle.
2025-02-27 23:12:08 +01:00
Spike 92d50dd73f Clerical edits to "Possible values.md" (#907)
* Massage Intro

Start with definition, then use cases, then valid syntax.

Use "legend" rather than "symbol" for the visible legend on the keyboard

Rephrase to avoid "exhaustive". Other simplifications.

* Edit section "Modifiers"

Separate into two tables, each preceded by relevant introduction

* Shuffle sections

Retitle "Special keys"; as used in the Intro, this term refers to all the keywords in this file.

Put two contrasting sections together, add "In contrast," and reword.
2025-02-27 22:12:59 +01:00
Jules Aguillon ba05b2770e Fix crash due to empty strings in keys 2025-02-27 22:00:32 +01:00
Sergiy Stupar 44d1343b83 Update Ukrainian translation (#909) 2025-02-27 21:32:44 +01:00
Jules Aguillon ca25cc55f6 Apply compose sequences to String keys
This is mostly useful for characters that do not fit on a single 16-bit
char.
Shift sequences for 𝕨𝕩𝕗𝕘𝕤 are added for illustration.
2025-02-23 18:00:44 +01:00
Jules Aguillon 68be82a4f9 Macro keys (#878)
Add "macro" keys that behave as if a sequence of keys is typed.

Macro can be added to custom layouts or through the "Add keys to the
keyboard option". The syntax is:

    symbol:key1,key2,..

The symbol cannot contain a : character. 'key1', 'key2', etc.. are:

  - 'String with \' escaping'
    The key will generate the specified string.
  - keyevent:123
    The key will send a keyevent.
  - The name of any special key
2025-02-23 12:12:29 +01:00
Jules Aguillon 581b31bf99 ExtraKeysPreference: Allow title to span several lines
Some titles in the extra keys list are truncated and some information
are invisible.
2025-02-22 12:02:04 +01:00
Jules Aguillon 56903084f2 Remove Japanese from method.xml
This makes the keyboard incorrectly reports that it supports Japanese.
2025-02-15 11:55:26 +01:00
Jules Aguillon ac8fff599f prefs: Add key description for dead keys 2025-02-15 11:51:35 +01:00
Zitrone a22cc39a45 layouts/latn_bone.xml: improve (#760)
* layouts/latn_bone.xml: use new syntax and add most missing keys

new syntax: n/s/e/w/ne/nw/se/sw instead of key{1..8}

Some keys were missing keys according to the tests, this adds them in a
similar way to latn_neo2.
Only `switch_forward` didn't fit (because of the 0
at the bottom of the spacebar).

* layouts/latn_bone.xml: even better bone

- use number row to unclutter rest of keyboard
- use modmap for special shift functions of bone layout
- add missing loc characters
- use standard bottom bar
- add diacritics from the 3 dead key keys on bone
- 2 small keys left and right of the number row (don't fit all 6
  diacritics, so they leak onto the number keys)
- row 1 is full too, so the dead keys from the left of that row leak
  onto the keys there as well

open questions:
- should bone rely on the math layer or use modmap?
- possibility to combine diacritics?
2025-02-15 11:28:21 +01:00
Luke Videckis 7e86ca70cd misc changes to deva alt layout (#880)
* misc changes to deva alt layout

* add indication for numpad for discoverability

* change key* notation to c/nw/ne/sw/se notation
2025-02-15 11:09:27 +01:00
Edgars 33f8bf9dea Fix typo in Latvian translation (#902) 2025-02-15 10:56:14 +01:00
Jules Aguillon 653c598a1c Refactor: Compute appearance values before onDraw
This moves some computations that used to be done during onDraw into the
new Theme.Computed class. This also removes Paint objects from the Theme
class, making it data-only.

This is a requirement for making some keys render differently.
2025-02-09 14:09:45 +01:00
Jules Aguillon bd5c815a6f Refactor: Cleanup KeyValue flag declarations
Flag 'FLAGS_OFFSET << 8' was incorrectly mentionned as free and the
value bits were not taking free flags into account and were bigger than
expected.
2025-02-09 13:13:15 +01:00
Jules Aguillon 06b76d58c2 Fix the compose key being lockable
The compose key was lockable because it had the flag "FLAG_SPECIAL".
Without this flag, the key is not lockable with a long press.
2025-02-09 13:10:16 +01:00
dzaima 255d7f80d7 Add swipe actions on number row (#892) 2025-02-09 12:53:49 +01:00
Jules Aguillon d3753cc455 Add a glyph for cancelling the compose key 2025-02-08 23:16:18 +01:00
Jules Aguillon 96dabde7c8 Make the left and right keys symbol smaller
Prevents the arrow symbols from colliding with the compose key symbol as
the dpad key is very narrow.
2025-02-08 23:14:01 +01:00
Jules Aguillon bd1a72d01e Improve the compose key symbol
Make the symbol bolder and slimmer.
2025-02-08 23:14:01 +01:00
Jules Aguillon 87667a2438 Remove binding fn+shift = capslock
This made is hard to type fn+shift.
gesture+shift still locks shift.
2025-02-08 22:16:37 +01:00
dzaima 8f729bb90a Add embedded_number_row keyboard attribute (#891) 2025-02-08 21:52:41 +01:00
Jules Aguillon aaf0a9a249 Improve the check_layout CI and output
Change the format of check_layout.output to avoid adding any logs for
layouts that do not trigger any warning.

Fix the check_layout CI, which was broken since check_layout.py was
changed to take arguments.
2025-02-04 17:17:46 +01:00
Jules Aguillon ae9c2fa626 check_layout.py: Adjust Shift, Tab and Esc checks 2025-02-04 16:46:49 +01:00
Jules Aguillon 0061911ef3 Pressing the compose key exits the pending sequence
Allows stopping a compose sequence without typing anything.
This is also a more intuitive behavior rather than starting a new
sequence.
2025-02-04 15:50:17 +01:00
Jules Aguillon 6b40803fa4 prefs: Mention Fn combination for accessing extra keys 2025-02-04 15:35:06 +01:00
dzaima defc4bc0bd Even slower slide 2025-02-03 12:49:32 +01:00
Jules Aguillon e1191f3add Fix Enter key sending message in Discord
Set the flag FLAG_SOFT_KEYBOARD when sending key events stops Discord
from sending message when typing Enter.
The other values are the same that are sent when using sendDownUpKeyEvents().
2025-01-25 23:20:05 +01:00
Jules Aguillon 188c68279d Slightly less permissive swipe detection
This allows starting an anticircle gesture on a key with sliders on two
opposite sides.
2025-01-25 22:31:23 +01:00
tenextractor 3e3fd6ffda Maltese layout (#825) 2025-01-25 22:31:04 +01:00
solokot 81c493bff2 Update Russian translation (#884) 2025-01-25 22:24:01 +01:00
David Holdeman e97dbbc771 Add Mongolian layout (#882) 2025-01-25 22:23:39 +01:00
tenextractor 1f9e450dfa Azerbaijani layout (#827) 2025-01-18 12:00:51 +01:00
Jules Aguillon b93bc90242 Vibrate when a circle gesture ends
This is especially useful when discovering the circle gestures and to
feel when a gesture has been entered by accident.
2025-01-18 11:45:13 +01:00
Jules Aguillon c6908ec846 Stable ordering or extra keys
Extra keys were ordered randomly everytime the settings were changed.
Now, there's a single ordering for each combinations of options in the
settings.
2025-01-18 11:34:08 +01:00
Jules Aguillon 16c2be96b3 latn_qwerty_gb: Better position for ',.' 2025-01-18 11:26:14 +01:00
Lokesh Kumar d4a6bfe122 Add /release to .gitignore 2025-01-18 11:13:35 +01:00
Jules Aguillon e3f9341ed1 Refactor: KeyValue: Simplify StringWithSymbol
This removes the Complex key kind and class by making StringWithSymbol a
new kind of key.
2025-01-12 23:24:04 +01:00
Jules Aguillon 3ea5c8d6b7 Refactor: KeyValue: Don't require _payload.equals
Don't require _payload.equals to be implemented (correctly) and avoids
inconsistencies with _payload.compareTo, which is required by type.
2025-01-12 23:10:40 +01:00
Jules Aguillonandgoyalyashpal f7c5b74940 Improve layout WORKMAN (US)
Co-authored-by: goyalyashpal <yashpalgoyal1304@gmail.com>
2025-01-12 23:09:47 +01:00
Jules Aguillon 4351b3eb6e Add "paste" to the pin entry layout 2025-01-12 21:45:10 +01:00
Jules Aguillon bffc76907a Add cursor_up and cursor_down slider keys
Implement up and down cursor movement slider. This is not added to any
layout yet due to the undesirable behavior when moving the focus out of
the text box being edited.
2025-01-12 19:16:04 +01:00
Jules Aguillon ca05c073d2 Allow sliders in every directions
Sliders are no longer limited to the x dimension and can be placed in
corners or purely on the y dimension.
2025-01-11 17:13:58 +01:00
Jules Aguillon ddd2eebb0e Remove the 'slider="true"' attribute
Whether a key behaves as a slider is now purely defined by the key
values present on it.
2025-01-11 16:30:36 +01:00
Jules Aguillon 1783dcdb35 Refactor: Restrict sliders to new 'Slider' key kind
Setting 'slider="true"' on a key is no longer enough to make a slider,
the key must also be of kind 'Slider'.

Only the KeyValue that started sliding is now considered, they can be
generated with negative values. This allows keys that don't have the
complementary cursor movement key on the opposite direction.

This will help implement other kind of sliders as well as up/down
sliders.
2025-01-11 16:18:58 +01:00
Jules Aguillon 4f8b5fa6ce Refactor: Make KeyValue payload Comparable
and use 'toString()' instead of 'getSymbol()'.

This removes unecessary casts when computing symbols and comparing key
values. This will make adding other kind of keys easier.
2025-01-11 15:54:24 +01:00
Jules Aguillon c4e2b446e5 Make the space bar slider slightly less sensitive 2025-01-11 14:24:10 +01:00
Jules Aguillon 7b8f739400 More precise 1-position movement with space bar slider
Moving the cursor by 1 position is hard because the finger has to travel
far enough to activate the slider but then becomes very sensitive.

With this change, swiping on the space bar slider will immediately move
the cursor by 1 position but the slider will activate only if the finger
travel an other swiping distance.
2025-01-11 14:12:43 +01:00
Jules Aguillon a3023a7f18 Lock shift with a gesture
The circle gesture locks shift. It is also the case for the round-trip
gesture if shift is in a corner.
2025-01-03 16:08:13 +01:00
Jules Aguillonand@srikanban 6ce2195253 Add glyphs for zwj and zwnj
Co-authored-by: @srikanban
2025-01-03 15:50:30 +01:00
Jules Aguillon 9ba718694b Refactor: Make all glyph file names 3 character long
For easier reading.
2025-01-03 15:04:33 +01:00
Jules Aguillon 11c9bd850b Remove numpad keys from the main layout
When the numpad is visible, remove the digits and arithmetic symbols
from the main layout. Similarly to when the number row is added.
2025-01-03 12:33:48 +01:00
Jules Aguillon b120fa8f09 Allow 'loc' keys to be present several times 2025-01-03 12:29:44 +01:00
Jules Aguillon e8c20bf521 test: Add ComposeKeyTest 2025-01-03 11:43:38 +01:00
Jules Aguillon 56c62a005d Add zwj, zwnj, nbsp, nnbsp to extra keys preferences 2025-01-02 11:08:04 +01:00
tenextractor 115eda4084 Lithuanian QWERTY layout (#824) 2025-01-02 10:43:01 +01:00
Jules Aguillon 55060f656e Don't double tap for capslock after automatic capitalisation
Allow to more easily disable automatic capitalisation when it's
unwanted.
2024-12-31 12:17:22 +01:00
Jules Aguillon 42c23d3864 Refactor: Simplify double tap for capslock
This doesn't fix a bug but remove some tricky code. The shift key is no
longer different when the "double tap for capslock" option is on.
The handling of the option is moved to Pointer instead and becomes
simpler.
2024-12-31 12:17:22 +01:00
Jules Aguillon f64a0be6fa Fn: redo and pasteAsPlainText as undo and paste
Allows typing redo and pasteAsPlainText easily with Fn or the round-trip
gesture without adding them to the keyboard.
2024-12-31 12:17:22 +01:00
srikanban b43cbf77c2 Update kann_kannada.xml (#858)
* Update kann_kannada.xml

Added zwnj & zwj
1. zwnj used for writing non Kannada words or names like Leo Tolstoy as ಲಿಯೊ ಟಾಲ್‌ಸ್ಟಾಯ್ (also can be written as ಲಿಯೊ ಟಾಲ್ಸ್ಟಾಯ್)
2. zwj used for writing ಅರ್ಕ as ಅರ‍್ಕ

Removed unecessary symbols not present in Kannada language:
1. ∪ is a set symbol, a similar symbol is used in Carnatic music, unfortunately not present in Unicode
2. ॰ used only in Devanagari
3. • bullet, not a character or alphabet in Kannada (Can't remember why I added it, maybe because I use it personally in non markdown texts)

* Update kann_kannada.xml

Cleaning up more
1. Removed ※, not found in other kannada keyboards
2. Moved `:` & `;`  on the key so that its easier to swipe
2024-12-31 10:51:58 +01:00
Jules Aguillon 568b5a99f6 Add preferred position for undo/cut/copy/paste
These keys are placed on the downward direction on the z/x/c/v keys when
they are enabled in the settings.
2024-12-31 10:42:23 +01:00
Jules Aguillon 12d9e5eaf8 Update Shift mappings for math symbols
Shift is the "not" version of some math operators.
∋ is added to the math layout.
2024-12-29 11:29:58 +01:00
tenextractor e594915e80 Uzbek layout (#828) 2024-12-29 10:56:54 +01:00
Jagadeeshan S 78974b7453 Tamil layout: Shift and fn changes (#853)
Signed-off-by: Jagadeeshan S <jagadeeshanmsj@gmail.com>
2024-12-29 10:48:43 +01:00
Jules Aguillon 1a4e61557f Release 1.30.3 (46)
Check layouts / Generated files (push) Has been cancelled
Check layouts / check_layout.output (push) Has been cancelled
Check translations / check-translations (push) Has been cancelled
Make Apk CI / Build-Apk (push) Has been cancelled
2024-12-29 10:31:02 +01:00
Jules Aguillon e13f58658d Fix crash in LayoutModifier 2024-12-29 10:20:18 +01:00
453 changed files with 23438 additions and 6557 deletions
View File
+39
View File
@@ -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
-26
View File
@@ -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
-16
View File
@@ -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
+26 -8
View File
@@ -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}}"
+12 -11
View File
@@ -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/
+3
View File
@@ -0,0 +1,3 @@
[submodule "vendor/cdict"]
path = vendor/cdict
url = https://github.com/Julow/cdict
+11 -3
View File
@@ -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 -->
+49 -15
View File
@@ -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": "Š"
}
}
```
+19 -4
View File
@@ -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.
Binary file not shown.
-199
View File
@@ -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"
}
}
+198
View File
@@ -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)
}
+32 -185
View File
@@ -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: <, >, ?, `, |, ~
+72 -31
View File
@@ -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)
+14 -6
View File
@@ -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.
+92 -78
View File
@@ -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`
`&` | `&amp;`
`<` | `&lt;`
`>` | `&gt;`
`"` | `&quot;`
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
:------- | :------
`&amp;` | `&`
`&lt;` | `<`
`&gt;` | `>`
`&quot;` | `"`
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 @@
لوحة مفاتيح افتراضية خفيفة الوزن ومراعية للخصوصية لنظام أندرويد.
+1
View File
@@ -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.
+1
View File
@@ -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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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 @@
คีย์บอร์ดสำหรับแอนดรอยด์ที่กินทรัพยากรน้อยและโฟกัสที่ความเป็นส่วนตัว
+1
View File
@@ -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
+91
View File
@@ -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": []
}
}
+3 -3
View File
@@ -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)
+189
View File
@@ -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()
+524
View File
@@ -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)
+3 -2
View File
@@ -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
Binary file not shown.
+3 -2
View File
@@ -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
Vendored
+182 -116
View File
@@ -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" "$@"
Vendored
+23 -18
View File
@@ -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
+67
View File
@@ -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
+99
View File
@@ -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.
+177
View File
@@ -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.
+180
View File
@@ -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>
+456
View File
@@ -0,0 +1,456 @@
# API Reference — libswipetype
> **Version:** 0.1.0 &nbsp;|&nbsp; **Language:** C++17 (core) &amp; Java (Android wrapper) &nbsp;|&nbsp; **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;
}
```
+357
View File
@@ -0,0 +1,357 @@
# Architecture — libswipetype
> **Version:** 0.1.0 &nbsp;|&nbsp; **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
+241
View File
@@ -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.
+17
View File
@@ -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
+220
View File
@@ -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()
+42
View File
@@ -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"

Some files were not shown because too many files have changed in this diff Show More