Compare commits

...

176 commits

Author SHA1 Message Date
Eran Leshem
b903e10b12
Add some missing emoji variants (#1622) 2025-05-31 10:44:46 +02:00
Helium314
e6a412750d update version and changelogs 2025-05-31 10:16:54 +02:00
Helium314
7b91221339 update translations, fix broken string references escaping the @ in @string/ 2025-05-31 10:06:03 +02:00
Helium314
f0d4aaa9c3 update reading dictionaries available for download 2025-05-31 09:58:29 +02:00
Helium314
0e86978df3 different way of implementing larger toolbar key
see GH-1556, fixes GH-1621
2025-05-31 09:35:33 +02:00
Eran Leshem
82bea7facf
Don't show dictionary debug info on toolbar (#1644) 2025-05-31 08:00:00 +02:00
Eran Leshem
4d441e5bdf
Fix partial emoji swiping (#1645) 2025-05-30 22:49:37 +02:00
Eran Leshem
92b1907c61
Drop non-emoji single character suggestions from emoji dictionary (#1643) 2025-05-30 22:40:52 +02:00
Helium314
37821ff8ad use old way of getting main dictionary suggestions on facilitator thread
apparently this fixes GH-1614 (spurious crash in the native google library on Android 7)
though maybe the reason is a difference between ExecutorUtils and coroutines?
2025-05-30 20:35:32 +02:00
Helium314
dbeddcd658 don't show tld hint label in numpad
see #1640
2025-05-30 20:19:26 +02:00
Helium314
31a8761bfa don't always use the same coordinate when creating fake ComposedData for a word
avoids some sort of cache returning previous suggestions, see GH-1542
2025-05-29 09:32:12 +02:00
Helium314
120734ff41 avoid issues when resetting DictionaryFacilitatorImpl
see GH-801 (probably does not fix the OnePlus issue)
2025-05-28 22:31:18 +02:00
Helium314
0d5159c2d7 comment currently unsupported keycodes to avoid confusion 2025-05-27 20:25:05 +02:00
Helium314
b44dd29b0c consider implicitly enabled subtypes when switching with language switch button
fixes GH-1608
2025-05-27 18:14:05 +02:00
Helium314
7da068145f fix internal dictionaty file names
resulted in assets dicts being extracted on every app start
2025-05-25 16:52:56 +02:00
Helium314
deec4d1f98 upgrade version, add changelog, fix lint issues with strings 2025-05-24 18:23:29 +02:00
Helium314
e21c135b90 don't include default latin non-qwerty layout in subtype display name
so now we're back at "French" and not "French (AZERTY)"
2025-05-24 16:50:51 +02:00
Helium314
154f7c3a1e don't create unnecessary additional subtypes when manually selecting qwerty 2025-05-24 16:46:18 +02:00
Helium314
ed540466e9 remove the xliff tags from stings
caused some issues with weblate, and meaning of %s is now in comment
2025-05-24 16:04:56 +02:00
Helium314
05ea8b7f76 remove unused strings (and rename a string) 2025-05-24 15:43:44 +02:00
Helium314
bf7e0542f5 remove the explicit "alphabet" from no_language (layout is more visible than previously) 2025-05-24 15:09:22 +02:00
Helium314
c4e7c84608 remove complicated and obsolete generic ubtype_with_layout_ and subtype_no_language_ strings
and simplify the place where they were used
2025-05-24 14:56:58 +02:00
Helium314
f72e8f41f4 make SubtypeLocaleUtils less convoluted
still harder to understand than necessary...
2025-05-24 12:45:36 +02:00
Helium314
69540b8d9f improve localizedDisplayName
don't use resoruces.getIdentifier
use override names e.g. for English (UK) for consistent layout names
merge with getLocaleDisplayNameInLocale
2025-05-24 11:35:58 +02:00
Helium314
d1120807d3 move InputMethodSubtype.displayName to SubtypeLocaleUtils 2025-05-24 09:29:11 +02:00
Helium314
9dbce40fd7 remove essentially duplicate functions 2025-05-24 09:14:35 +02:00
Helium314
82e6d8a5cb move SubtypeLocaleUtils to Kotlin 2025-05-24 08:25:18 +02:00
Bernardo do Amaral Teodosio
175b5ea197
Fix Wrong "\" in pt-BR translations for the onboarding (#1581) 2025-05-24 07:09:11 +02:00
Helium314
917edee918 fix obviously broken links in description
fixes GH-1568
2025-05-24 07:04:49 +02:00
Helium314
ead8fb36cb update translations 2025-05-24 07:01:05 +02:00
Helium314
3f51bd4da5 tune layout edit dialog 2025-05-23 21:25:37 +02:00
Helium314
7bc74810b1 fix broken file handling in DictionaryGroup 2025-05-23 20:42:52 +02:00
Eran Leshem
e25300d832
Add comma to the repurposed comma key's popup in URL and email mode (#1594) 2025-05-21 22:48:33 +02:00
Helium314
e034065236 add simple DicitonaryFacilitator for a single dictionary (to be used later) 2025-05-21 22:43:01 +02:00
Helium314
954a27b7c9 refactor creation of main dictionary 2025-05-21 22:13:53 +02:00
Helium314
b1b357d6b8 move DictionaryInfoUtils to Kotlin 2025-05-21 21:02:16 +02:00
Helium314
e32a0c8e98 move ComposedData to Kotlin
and add method to create ComposedData for a given word
2025-05-21 18:13:58 +02:00
Helium314
d9a779a66e update comments and documentation
currently irrelevant changes to dump methods
2025-05-21 17:35:24 +02:00
Helium314
18549151b3 add color for popup key icons
fixes GH-1577
2025-05-20 23:05:01 +02:00
Eran Leshem
9d38471f72
Enlarge toolbar button and icon (#1556) 2025-05-20 22:57:02 +02:00
Helium314
4289e487e9 prepare for adding customizable weights for user-provided dictionaries 2025-05-20 22:17:10 +02:00
Helium314
27a2300631 remove unused "account" 2025-05-20 21:32:23 +02:00
Helium314
900dfa1b9c no need for special lock object 2025-05-20 21:18:38 +02:00
Helium314
9709c0d0a2 more detailed comment on feature 2025-05-20 21:17:08 +02:00
Helium314
960f058b7e move DictionaryFacilitatoryImpl to Kotlin
only very minor changes to behavior
using coroutines instead of ExecutorUtils
some code moved out of "main" facilitator
2025-05-20 20:44:57 +02:00
Helium314
4ecf185431 add label for timestamp key 2025-05-18 22:03:04 +02:00
Helium314
e45f0660a2 process label for simple popup keys
now toolbar keys and defined key labels work in simple layout popup keys
2025-05-18 21:52:25 +02:00
Helium314
e7ccf72fc5 reduce unnecessary json parse attempts when editing layouts 2025-05-18 21:32:53 +02:00
Helium314
e154001d44 fix broken links, remove outdated information 2025-05-18 20:48:28 +02:00
Henré Botha
44558ceeaa
Fix broken internal link (#1574) 2025-05-18 20:44:23 +02:00
Helium314
aa8068b5d2 functional key background now also sets functional key text color
for consistency, fixes GH-1576
2025-05-18 20:15:16 +02:00
Helium314
466ecfb78c reload keyboard theme when changing number row setting
otherwise emoji keyboards are not properly reloaded and may end up misaligned
2025-05-18 19:20:07 +02:00
Eran Leshem
731c6cdd5e
Allow for switching between emoji categories using swipe left/right (#1488) 2025-05-18 19:18:23 +02:00
Helium314
199f177c2d add left/right variants of alt, ctrl and meta
for picky apps, fixes GH-1579
2025-05-18 15:25:10 +02:00
Helium314
66c3dd7a81 use correct checksum 2025-05-17 20:33:31 +02:00
Helium314
9c9fe392d1 fix dealing with prases without non-whitespace letters in SpacedTokens 2025-05-17 16:56:22 +02:00
Helium314
c33c2c5823 remove test logging 2025-05-17 09:54:27 +02:00
Helium314
4d91702073 add setting for default emoji skin tone
fixes GH-817
2025-05-16 21:48:07 +02:00
Helium314
a0f77c1392 don't suggest unsuported emojis
and invert isSupported to isUnsupported to avoid confusion what happens when asking about non-emojis
2025-05-16 21:20:34 +02:00
Helium314
35df3e7bae upgrade dependencies 2025-05-16 21:09:16 +02:00
Helium314
f48438f30a use SpacedTokens instead of regex for splitting on whitespace
and where possible directly used SpacedTokens to avoid unnecessary list
2025-05-14 17:04:31 +02:00
Helium314
c96eec601d remove unused code / comments in EmojiPalettesView and related resources 2025-05-14 16:49:37 +02:00
Devy Ballard
c9059f3616
Apps dictionary (#1361) 2025-05-14 16:41:50 +02:00
Devy Ballard
2fe87eea9b
Inline code point loops (#1408) 2025-05-14 16:24:21 +02:00
Eran Leshem
c4386df186
Display a toast after restoring a backup. Seems like useful feedback. (#1531) 2025-05-14 16:23:39 +02:00
Helium314
95d4bfe97c
Update managing emoji categories (#1515) 2025-05-14 16:22:50 +02:00
Helium314
880c7eaf33 update version and translations 2025-05-14 16:11:52 +02:00
Helium314
b5837c3380 try saving crash logs when device is locked 2025-05-14 15:58:24 +02:00
Helium314
e6ec1c7bca fix debug mode crash sometimes occurring when deleting korean
fixes GH-1551
2025-05-10 17:37:20 +02:00
Helium314
549675d8d7 change query for other IME packages
should fix GH-1340
2025-05-10 15:50:58 +02:00
Helium314
a1e05c847e no need for full stack trace when system gesture lib is not found 2025-05-10 09:27:25 +02:00
Eran Leshem
1f8a94f219
Adjust emoji key size according to emoji font size (#1543) 2025-05-09 16:49:23 +02:00
Helium314
3c36033acb add more information to exception when InputMethodInfo is not found
apparently this randomly happens, see GH-1521
2025-05-09 05:16:22 +02:00
Helium314
97db67d7eb increase contacts dictionary frequency
because contacts rarely got suggested, see GH-1361
2025-05-07 18:23:47 +02:00
Helium314
a3dff524cb avoid problems with hacky way of saving AllColors theme name
see GH-1528
2025-05-06 18:58:14 +02:00
Helium314
366ee5ae28 workaround for page start / page end toolbar keys not working in compose text fields
fixes GH-1477
2025-05-04 20:33:41 +02:00
Helium314
91b177d204 update version, translations and dependencies 2025-05-03 07:52:16 +02:00
Eran Leshem
4f356086d7
Fix direction of word-left & word-right with RTL scripts (#1530) 2025-05-03 07:02:38 +02:00
Eran Leshem
60a5fe1e03
Go back to setFitsSystemWindows, with an added workaround that seems to make it reliable. (#1536) 2025-05-02 23:11:58 +02:00
Helium314
d8bf27f180 add missing items in v31 dialog theme
resolves GH-1518
2025-05-02 05:35:27 +02:00
Helium314
2a7ac3cf79 use default font for titles 2025-05-01 20:27:56 +02:00
Eran Leshem
875491a0e1
Recognize the he language code as Hebrew (#1529)
in addition to outdated `iw` that is used on Android 14 and older
2025-05-01 20:17:41 +02:00
Helium314
9f06394a1a consistent indent 2025-04-29 19:36:47 +02:00
Froingo
ad375cc3a3
Add korean phonetic layout (#1500) 2025-04-29 19:32:03 +02:00
Helium314
011bc96ec9 consider padding when adding keyboard split
fixes GH-1520
2025-04-29 16:17:28 +02:00
Helium314
da62457c90 save logs from the internal logger again, but keep logcat for getting older warning and error log entries 2025-04-28 21:20:32 +02:00
Helium314
38547b0c81 fallback to default layouts on parsing errors 2025-04-28 19:50:31 +02:00
Helium314
8b36ff1c54 skip early on empty hint labels 2025-04-28 19:25:28 +02:00
Eran Leshem
5eff3b992b
Restore Key.LABEL_FLAGS_PRESERVE_CASE, to fix uppercase TLDs in popup (#1517) 2025-04-28 14:27:36 +02:00
Helium314
b5dece2ff4 improve behvavior of slider preference
using onConfirm instead of always onValueChange (which is called when sliding, but we're mostly interested in changed settings)
2025-04-28 13:56:36 +02:00
Helium314
1d441a8ca6 more workarounds for Korean
not just space might delete text, but also other separators
now this problem is avoided in a more generic way
fixes GH-1447
2025-04-27 18:59:29 +02:00
Eran Leshem
9c727f342d
Remove TLD popup hint (#1511)
Co-authored-by: Helium314 <helium314@mailbox.org>
2025-04-27 16:19:26 +02:00
Eran Leshem
bedb9d1517
Fix settings search insets by moving top & horizontal insets to the top of the setting screen. (#1510) 2025-04-27 16:03:18 +02:00
Helium314
54c2c364a0 close background image dialog when deleting image 2025-04-27 15:44:59 +02:00
Helium314
106a74d749 upgrade version and translations 2025-04-26 13:55:40 +02:00
Helium314
322f8f9712 also consider default layouts when removing custom layouts without file
was missing in d15a97ccba
also rearrange the code a little
fixes GH-1490
2025-04-26 13:24:10 +02:00
Eran Leshem
6d9f69a4b6
Fix keyboard insets on Android 15, again (#1497) 2025-04-26 07:34:27 +02:00
Gabriele Monaco
14b5439a97
Use the euro sign for PMS (#1504) 2025-04-25 20:04:05 +02:00
Eran Leshem
01c0cd9de2
Saving log using logcat (#1487)
Old logger will be removed later, see discussion in PR
2025-04-25 17:19:58 +02:00
Helium314
e60efba59d remove some unnecessary code
looks like with edge to edge the status bar color is set automatically
also slightly rearrange code for showing welcome wizard and crash report dialog
2025-04-25 17:03:34 +02:00
Eran Leshem
5b32118b08
Add title to spell checker settings (#1501) 2025-04-25 16:53:22 +02:00
Eran Leshem
69bcca0a22
Apply padding on all sides on settings screens (#1496) 2025-04-22 22:29:03 +02:00
Helium314
49ed863a7e upgrade version and translations 2025-04-21 10:23:49 +02:00
Helium314
46f9227615 properly reload emoji keyboards (fixes split layout change not shown) 2025-04-21 09:48:22 +02:00
Helium314
7e59bcc799 remove weird workaround to get acceptable layout edit dialog positioning
not necessary any more after enabling edge to edge
2025-04-21 08:29:50 +02:00
Eran Leshem
d9f17733d9
Make settings screens fully usable on Android 15 (#1484)
enable edge to edge in settings for all Android versions to avoid minor glitches and have more consistent appearance
2025-04-21 08:06:41 +02:00
Helium314
d15a97ccba consider all layout types when checking for custom layouts without file
fixes #1490
2025-04-21 07:37:42 +02:00
Helium314
da7ab05920 remove old upgrade functionality done for migration from previous package name
should be fine after more than a year of HeliBoard
fixes issue when restoring custom layouts

upgrade now would need to be OpenBoard -> HeliBoard <= 2.3 -> HeliBoard >= 3.0
2025-04-21 07:33:58 +02:00
Helium314
d87ed8e53d fix pattern for files to back up
see #1490
2025-04-21 06:03:32 +02:00
Eran Leshem
1012386c8c
more reliable fix for bottom inset padding on edge-to-edge (#1486) 2025-04-20 18:58:58 +02:00
Helium314
7748ed75fe clarify when we use our own name for the language instead of using whatver is provided by the system 2025-04-19 08:35:46 +02:00
Benson Muite
c32b3bada4
Keyboard layouts for languages primarily used in Africa (#1483) 2025-04-19 08:27:58 +02:00
Helium314
e042adc5b8 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-04-18 10:04:19 +02:00
Eran Leshem
901e745158
Fix bottom of keyboard being under navigation bar (#1461)
mostly for Android 15+, but issues may also occur on older Android versions
2025-04-18 10:02:27 +02:00
Helium314
91554b02eb fix unchanged layout list when deleting secondary layout 2025-04-14 18:46:41 +02:00
Helium314
fe7f1a1b38 fix crash when deleting subtype
fixes GH-1476
2025-04-14 17:06:34 +02:00
Helium314
8fddf94121 fix unable to change one-handed mode scale 2025-04-08 16:10:04 +02:00
Helium314
003ec854ab update version 2025-04-06 19:20:01 +02:00
Helium314
00ae92318d add missed changelog translations 2025-04-06 18:33:06 +02:00
Helium314
e4cd58a722 update translations 2025-04-06 18:32:23 +02:00
Eran Leshem
f4b4705e81
Add bottom inset padding on Android15+ (#1457) 2025-04-06 10:54:39 +02:00
Helium314
d4960c73dc fix tests after recent changes 2025-04-06 10:52:24 +02:00
Helium314
22eb48ff91 add comments about build variants 2025-04-06 10:50:10 +02:00
Helium314
6995266bd1 treat default button as value change for SliderPreference 2025-04-05 10:24:24 +02:00
Eran Leshem
087f87e95c
Optimize debug build (#1411) 2025-04-05 08:43:24 +02:00
Helium314
8edea4f7c5 allow renaming main layouts in subtypeScreen 2025-04-03 17:09:27 +02:00
Helium314
d79c84d7df fix issues when renaming or deleting layouts 2025-04-03 17:09:15 +02:00
Helium314
57deb82ca7 remove todo section from readme 2025-03-31 20:13:28 +02:00
Helium314
7a57f5a24f make searchText non-saveable and add a comment 2025-03-31 20:12:52 +02:00
Helium314
452770566c avoid creating additional subtypes that are the same as a resource subtype 2025-03-31 16:31:52 +02:00
Helium314
8247366bdd avoid issues with prefChanged not changing 2025-03-31 16:27:16 +02:00
Helium314
3dbd9c6ed9 slightly adjust behavior and layouts of subtype screen 2025-03-31 01:27:06 +02:00
Helium314
6bbce0b5ca change subtype dialog to screen
mostly just moved the content and adjusted necessary calls
still needs some minor tweaking
2025-03-30 12:40:06 +02:00
Helium314
ac805a9286 fix some issues with renaming color schemes
fixes GH-1449 and some more unreported bugs
2025-03-30 12:03:21 +02:00
Helium314
8932fc84e1 add timestamp keycode and setting for adjusting format, fixes GH-846 2025-03-29 12:36:07 +01:00
Helium314
525c4e59b6 confirm color in color picker dialog on done, fixes GH-1443 2025-03-29 11:47:06 +01:00
Helium314
a3fcce26a7 add all_colors color for popup key text, fixes #1297 2025-03-29 10:48:14 +01:00
Helium314
58778b1f23 add all_colors color for key preview text, fixes #1434 2025-03-29 10:38:43 +01:00
Helium314
fbfff03541 add all_colors color for key text in emoji keyboard, fixes #1058 2025-03-29 10:18:27 +01:00
Helium314
a1f991088d reduce font size for multilibgual typing in settings 2025-03-29 07:19:17 +01:00
Helium314
10af5def2b deal with some very bad behavior of firefox, fixes #1139 2025-03-27 21:04:42 +01:00
Helium314
a745c92e05 update ndk and translations 2025-03-26 21:26:05 +01:00
Helium314
b26ba76221 add test related to GH-1408 2025-03-26 20:57:54 +01:00
Helium314
1b1dbd4006 upgrade robolectric version to make tests work again 2025-03-26 20:38:06 +01:00
Helium314
f06521c8ec make text fields follow content language direction 2025-03-26 20:07:47 +01:00
Helium314
0847bac3d5 upgrade target SDK to 35
might cause issues with hebrew and indonesian on Android 15 devices due to changed language codes
2025-03-26 19:56:46 +01:00
Helium314
f5bc89b91d make reverting autocorrect on backspace optional
fixes GH-210
2025-03-26 19:18:13 +01:00
Helium314
c0b14635fd fix issue that could result in autospace being added before period 2025-03-26 18:29:57 +01:00
Helium314
b600431af9 reduce vertical padding for preference 2025-03-26 18:11:47 +01:00
Helium314
66a07eb8d2 Add more settings for autospace
fixes GH-1348
fixes GH-876
2025-03-26 18:05:00 +01:00
EduRGB
88a7f41038
Layout for shift + long press on number row (#1400)
Add full-featured number row layout and set it as default; set existing layout as basic.
2025-03-22 20:28:56 +01:00
Helium314
2fe0937ead apparently people want smaller icons 2025-03-22 20:05:51 +01:00
Helium314
efaddf6c51 store clipboard history as strings, fixes GH-1430 2025-03-22 19:34:27 +01:00
Helium314
9e91e7562b add another setting to disable always show suggestions just for web edit fields, which seem fo frequenty cause issues with this setting 2025-03-22 19:27:38 +01:00
Helium314
7228fa06d1 move "no limit" clipboard history retantion time to the right side
sort of fixes GH-1425 (at least the unclarity about finding the setting)
2025-03-22 19:05:24 +01:00
Helium314
a3bada8d25 upgrade version and translations 2025-03-16 15:01:39 +01:00
Helium314
9b7eaa4cf2 use default text button style for popup key order / hint source buttons 2025-03-16 14:49:04 +01:00
Helium314
7571890551 remove appcompat
was mainly used for settings
2025-03-16 14:33:53 +01:00
KuRa KuRd
cb70553484
Add Central Kurdish layout (#1417) 2025-03-16 14:00:23 +01:00
Helium314
8298542c39 add setting to switch to alphabet after typing on numpad and pressing space
fixes #1224
2025-03-15 15:59:49 +01:00
Helium314
a9e5f879d8 better way of determining whether a key should have action key background
mostly relevant for popups
fixes #1008
2025-03-15 15:35:55 +01:00
Quantom2
a6b6d1b659
Rework of UK and RU standard layout plus added extended layout (#1215)
Rework of UK and RU standard layout plus added extended layout (similar to PC keyboard) as optional;
- Added 'ї' as separate letter (it used much more often, just to be optional key in popups)
- Added [ { } } popups to fill empty hint space (similar to PC layout)
- Added ' (apostrofee) suggestion onto 'є' key (similar to oter keyboards and to PC layout)
RU)
Added siggestion to 'ъ' on 'х' key (similar to other keyboards, anyway there was no popups at all)
- Added [ { } } popups to fill empty hint space (similar to PC layout)
- Added 'э́' suggestion similar to other keyboards
Both)
- Improvements to multilanguage typing (now this is possible to sue UK-RU or RU-UK pair, I added letters from other language as popups)
Added)
- UK Extended with separate ' key (used less often than 'ї', but still used pretty often)
- RU Extended, with separate 'ъ' key (similar to full desk PC layout)
2025-03-15 14:05:05 +01:00
Helium314
ba88129641 make navigation transistion animation duration follow system transition animation scale 2025-03-15 09:06:10 +01:00
Helium314
55259b2915 fix disappearing all_colors for dark theme on upgrade from 2.3 2025-03-14 23:21:41 +01:00
Helium314
d3401e5c04 re-add background to app icon in about screen 2025-03-14 22:57:00 +01:00
Eran Leshem
18a328cd2b
Show TLD popup keys (#1399)
* Add option to replace period key's popup keys with the locale-specific top level domains for URL and email inputs. On by default.
Also change the wide keyboard's '.com' key to use the same TLD list.
Tweak TLD list order, putting default ones first, so that .com is the first TLD.

* Enable TLD hint
Avoid TLD popups on tablet
Revert to using set for TLDs, and insert default ones first
Move setting one slot up
Tweak setting description
Update docs

* Preserve case on period key

* Prevent non-TLD hints on period key
2025-03-13 16:31:03 +01:00
unlair
d05a59e4ed
Track overridden subtypes when auto-switching due to HintLocales (#1410)
* Track overridden subtypes when auto-switching due to HintLocales

* Move hint-locale switching logic to SubtypeState
2025-03-11 21:25:32 +01:00
Helium314
bf713d6967 copy user-supplied library to final file instead of renaming
apparently fixes #1251
2025-03-10 18:27:45 +01:00
Helium314
e9a2a7ebb1 remove unnecessary settings reloads during adjusting additional subtype 2025-03-08 17:55:02 +01:00
Helium314
2b8c39b125 make editing the "+" layouts work 2025-03-08 10:26:24 +01:00
Helium314
c47da4203f consistent way of for settingsSubtypes <-> string 2025-03-08 09:34:33 +01:00
Helium314
e1f02dab31 add some tests for subtype enablement 2025-03-08 09:23:41 +01:00
Helium314
a4d96a12a9 fix some issues with subtypes being disabled or not shown as enabled after editing 2025-03-08 09:05:33 +01:00
Helium314
15c1526895 toAdditional subtype not nullable any more
was causing too much trouble
and only was in there because "it always has been there"
2025-03-08 07:48:15 +01:00
Helium314
fa9ac20d39 fix edited subtypes not showing up when they weren't enabled 2025-03-08 07:35:49 +01:00
Helium314
912ba45d5e fix default additional subtypes 2025-03-08 07:35:08 +01:00
377 changed files with 11016 additions and 10615 deletions

View file

@ -14,7 +14,6 @@ Does not use internet permission, and thus is 100% offline.
* [Translations](#translations)
* [To Community Creation](#to-community)
* [Code Contribution](CONTRIBUTING.md)
- [To-do](#to-do)
- [License](#license)
- [Credits](#credits)
@ -41,7 +40,7 @@ Does not use internet permission, and thus is 100% offline.
</ul>
<li>Clipboard history</li>
<li>One-handed mode</li>
<li>Split keyboard (only available if the screen is large enough)</li>
<li>Split keyboard</li>
<li>Number pad</li>
<li>Backup and restore your settings and learned word / history data</li>
</ul>
@ -88,26 +87,6 @@ You can share your themes, layouts and dictionaries with other people:
## Code Contribution
See [Contribution Guidelines](CONTRIBUTING.md)
# To-do
__Planned features and improvements:__
* Improve support for modifier keys (_alt_, _ctrl_, _meta_ and _fn_), some ideas:
* keep modifier keys on with long press
* keep modifier keys on until the next key press
* use sliding input
* Less complicated addition of new keyboard languages (e.g. #519)
* Additional and customizable key swipe functionality
* Some functionality will not be possible when using glide typing
* Add and enable emoji dictionaries by default (if available for language)
* Clearer / more intuitive arrangement of settings
* Maybe hide some less used settings by default (similar to color customization)
* Make use of the `.com` key in URL fields (currently only available for tablets)
* With language-dependent TLDs
* [Bug fixes](https://github.com/Helium314/HeliBoard/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
__What will _not_ be added:__
* Dictionaries for more languages (you can still download them)
* Anything that requires additional permissions, unless there is a _very_ good reason
# License
HeliBoard (as a fork of OpenBoard) is licensed under GNU General Public License v3.0.

View file

@ -1,20 +1,19 @@
plugins {
id("com.android.application")
kotlin("android")
kotlin("plugin.serialization") version "2.0.21"
kotlin("plugin.serialization") version "2.1.21"
kotlin("plugin.compose") version "2.0.0"
}
android {
compileSdk = 34
buildToolsVersion = "34.0.0"
compileSdk = 35
defaultConfig {
applicationId = "helium314.keyboard"
minSdk = 21
targetSdk = 34
versionCode = 3000
versionName = "3.0-alpha1"
targetSdk = 35
versionCode = 3101
versionName = "3.1"
ndk {
abiFilters.clear()
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))
@ -36,14 +35,23 @@ android {
isJniDebuggable = false
}
debug {
// "normal" debug has minify for smaller APK to fit the GitHub 25 MB limit when zipped
// and for better performance in case users want to install a debug APK
isMinifyEnabled = true
isJniDebuggable = false
applicationIdSuffix = ".debug"
}
create("runTests") { // build variant for running tests on CI that skips tests known to fail
isMinifyEnabled = true
isMinifyEnabled = false
isJniDebuggable = false
}
create("debugNoMinify") { // for faster builds in IDE
isDebuggable = true
isMinifyEnabled = false
isJniDebuggable = false
signingConfig = signingConfigs.getByName("debug")
applicationIdSuffix = ".debug"
}
base.archivesBaseName = "HeliBoard_" + defaultConfig.versionName
}
@ -58,9 +66,9 @@ android {
path = File("src/main/jni/Android.mk")
}
}
ndkVersion = "26.2.11394342"
ndkVersion = "28.0.13004108"
packagingOptions {
packaging {
jniLibs {
// shrinks APK by 3 MB, zipped size unchanged
useLegacyPackaging = true
@ -96,29 +104,29 @@ android {
dependencies {
// androidx
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.autofill:autofill:1.1.0")
implementation("androidx.viewpager2:viewpager2:1.1.0")
// kotlin
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
// compose
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
implementation(platform("androidx.compose:compose-bom:2025.02.00"))
implementation(platform("androidx.compose:compose-bom:2025.05.00"))
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.navigation:navigation-compose:2.8.8")
implementation("androidx.navigation:navigation-compose:2.9.0")
implementation("sh.calvin.reorderable:reorderable:2.4.3") // for easier re-ordering
implementation("com.github.skydoves:colorpicker-compose:1.1.2") // for user-defined colors
// test
testImplementation(kotlin("test"))
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.15.2")
testImplementation("org.robolectric:robolectric:4.12.1")
testImplementation("org.mockito:mockito-core:5.17.0")
testImplementation("org.robolectric:robolectric:4.14.1")
testImplementation("androidx.test:runner:1.6.2")
testImplementation("androidx.test:core:1.6.1")
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-License-Identifier: GPL-3.0-only
-->
<resources>
<string name="english_ime_name" translatable="false">HeliBoard debug</string>
<string name="spell_checker_service_name" translatable="false">HeliBoard debug Spell Checker</string>
<string name="ime_settings" translatable="false">HeliBoard debug Settings</string>
</resources>

View file

@ -98,7 +98,15 @@ SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
<queries>
<!-- To detect other IMEs -->
<intent>
<action android:name="android.view.InputMethod"/>
<!-- changed to * as it's supposed to help with finding other keyboard apps, see https://github.com/Helium314/HeliBoard/issues/1340 -->
<!--<action android:name="android.view.InputMethod" />-->
<action android:name="*" />
</intent>
<!-- To detect names of installed apps -->
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
</manifest>

View file

@ -1,114 +1,116 @@
main,af,exp
main,ar,
main,ar,exp
main,hy,
main,as,
main,bn_BD,exp
main,bn,
main,bn,exp
main,eu,
main,be,
main,bg,
main,bg,exp
main,ca,
main,ca,exp
main,hr,
main,hr,exp
main,cs,
main,cs,exp
main,da,
main,da,exp
main,nl,
main,nl,exp
main,en_AU,
main,en_CA,exp
main,en_GB,
main,en_GB,exp
main,en_US,
main,en_US,exp
symbols,en,exp
emoji,en,
main,eo,
main,eo,exp
main,et,exp
main,fi,
main,fi,exp
emoji,fr,
symbols,fr,exp
main,fr,
main,fr,exp
main,gl,
main,gl,exp
main,ka,
main,de_AT,exp
main,de_CH,
main,de,
main,de,exp
main,gom,
main,el,
main,gu,
main,he,
main,iw,
main,he,exp
main,hi,
main,hi_ZZ,
main,hu,
main,hu,exp
main,is,exp
main,id,exp
main,it,
main,it,exp
main,kab,exp
main,kn,
main,ks,
main,kk,exp
main,km,
main,la,
main,lv,
main,lv,exp
main,lt,
main,lt,exp
main,lb,
main,mai,
addon,ml_ZZ,exp
main,ml,
main,mr,
main,ne,exp
main,nb,
main,nb,exp
main,or,
main,pms,exp
main,pl,
main,pl,exp
main,pt_BR,
main,pt_PT,
main,pt_PT,exp
main,pa,
main,ro,
main,ro,exp
emoji,ru,
main,ru,
main,ru,exp
main,sa,
main,sat,
main,sr_ZZ,
main,sr,
main,sd,
main,sk,exp
main,sl,
main,sl,exp
main,es,
main,es,exp
main,zgh_ZZ,
main,zgh,
main,sv,
main,sv,exp
main,ta,
main,te,
main,tok,
main,tcy,
main,tr,
main,tr,exp
emoji,uk,
main,uk,
main,ur,
main,af,exp
main,ar,exp
main,bn_BD,exp
main,bn,exp
main,bg,exp
main,ca,exp
main,hr,exp
main,cs,exp
main,da,exp
main,nl,exp
main,en_GB,exp
main,en_US,exp
symbols,en,exp
main,eo,exp
main,et,exp
main,fi,exp
symbols,fr,exp
main,fr,exp
main,gl,exp
main,de_AT,exp
main,de,exp
main,he,exp
main,hu,exp
main,is,exp
main,id,exp
main,it,exp
main,kab,exp
main,kk,exp
main,lv,exp
main,lt,exp
addon,ml_ZZ,exp
main,ne,exp
main,nb,exp
main,pms,exp
main,pl,exp
main,pt_PT,exp
main,ro,exp
main,ru,exp
main,sk,exp
main,sl,exp
main,es,exp
main,sv,exp
main,tr,exp
main,uk,exp
main,ur,
main,vi,exp

1 main ar af exp
1 main af exp
2 main ar ar
3 main ar exp
4 main hy hy
5 main as as
6 main bn_BD exp
7 main bn bn
8 main bn exp
9 main eu eu
10 main be be
11 main bg bg
12 main bg exp
13 main ca ca
14 main ca exp
15 main hr hr
16 main hr exp
17 main cs cs
18 main cs exp
19 main da da
20 main da exp
21 main nl nl
22 main nl exp
23 main en_AU en_AU
24 main en_CA exp
25 main en_GB en_GB
26 main en_GB exp
27 main en_US en_US
28 main en_US exp
29 symbols en exp
30 emoji en en
31 main eo eo
32 main eo exp
33 main et exp
34 main fi fi
35 main fi exp
36 emoji fr fr
37 symbols fr exp
38 main fr fr
39 main fr exp
40 main gl gl
41 main gl exp
42 main ka ka
43 main de_AT exp
44 main de_CH
45 main de de
46 main de exp
47 main gom gom
48 main el el
49 main gu gu
50 main he he
51 main iw iw
52 main he exp
53 main hi hi
54 main hi_ZZ hi_ZZ
55 main hu hu
56 main hu exp
57 main is exp
58 main id exp
59 main it it
60 main it exp
61 main kab exp
62 main kn kn
63 main ks ks
64 main kk exp
65 main km km
66 main la la
67 main lv lv
68 main lv exp
69 main lt lt
70 main lt exp
71 main lb lb
72 main mai mai
73 addon ml_ZZ exp
74 main ml ml
75 main mr mr
76 main ne exp
77 main nb nb
78 main nb exp
79 main or or
80 main pms exp
81 main pl pl
82 main pl exp
83 main pt_BR pt_BR
84 main pt_PT pt_PT
85 main pt_PT exp
86 main pa pa
87 main ro ro
88 main ro exp
89 emoji ru ru
90 main ru ru
91 main ru exp
92 main sa sa
93 main sat sat
94 main sr_ZZ sr_ZZ
95 main sr sr
96 main sd sd
97 main sk exp
98 main sl sl
99 main sl exp
100 main es es
101 main es exp
102 main zgh_ZZ zgh_ZZ
103 main zgh zgh
104 main sv sv
105 main sv exp
106 main ta ta
107 main te te
108 main tok tok
109 main tcy tcy
110 main tr tr
111 main tr exp
112 emoji uk uk
113 main uk uk
main ur
main af exp
main ar exp
main bn_BD exp
main bn exp
main bg exp
main ca exp
main hr exp
main cs exp
main da exp
main nl exp
main en_GB exp
main en_US exp
symbols en exp
main eo exp
main et exp
main fi exp
symbols fr exp
main fr exp
main gl exp
main de_AT exp
main de exp
main he exp
main hu exp
main is exp
main id exp
main it exp
main kab exp
main kk exp
main lv exp
main lt exp
addon ml_ZZ exp
main ne exp
main nb exp
main pms exp
main pl exp
main pt_PT exp
main ro exp
main ru exp
main sk exp
main sl exp
main es exp
main sv exp
main tr exp
114 main uk uk exp exp
115 main ur
116 main vi vi exp exp

View file

@ -0,0 +1,85 @@
🎃
🎄
🎆
🎇
🧨
🎈
🎉
🎊
🎋
🎍
🎎
🎏
🎐
🎑
🧧
🎀
🎁
🎗️
🎟️
🎫
🎖️
🏆
🏅
🥇
🥈
🥉
🥎
🏀
🏐
🏈
🏉
🎾
🥏
🎳
🏏
🏑
🏒
🥍
🏓
🏸
🥊
🥋
🥅
⛸️
🎣
🤿
🎽
🎿
🛷
🥌
🎯
🪀
🪁
🔫
🎱
🔮
🪄
🎮
🕹️
🎰
🎲
🧩
🧸
🪅
🪩
🪆
♠️
♥️
♦️
♣️
♟️
🃏
🀄
🎴
🎭
🖼️
🎨
🧵
🪡
🧶
🪢

View file

@ -0,0 +1,159 @@
🐵
🐒
🦍
🦧
🐶
🐕
🦮
🐕‍🦺
🐩
🐺
🦊
🦝
🐱
🐈
🐈‍⬛
🦁
🐯
🐅
🐆
🐴
🫎
🫏
🐎
🦄
🦓
🦌
🦬
🐮
🐂
🐃
🐄
🐷
🐖
🐗
🐽
🐏
🐑
🐐
🐪
🐫
🦙
🦒
🐘
🦣
🦏
🦛
🐭
🐁
🐀
🐹
🐰
🐇
🐿️
🦫
🦔
🦇
🐻
🐻‍❄️
🐨
🐼
🦥
🦦
🦨
🦘
🦡
🐾
🦃
🐔
🐓
🐣
🐤
🐥
🐦
🐧
🕊️
🦅
🦆
🦢
🦉
🦤
🪶
🦩
🦚
🦜
🪽
🐦‍⬛
🪿
🐦‍🔥
🐸
🐊
🐢
🦎
🐍
🐲
🐉
🦕
🦖
🐳
🐋
🐬
🦭
🐟
🐠
🐡
🦈
🐙
🐚
🪸
🪼
🦀
🦞
🦐
🦑
🦪
🐌
🦋
🐛
🐜
🐝
🪲
🐞
🦗
🪳
🕷️
🕸️
🦂
🦟
🪰
🪱
🦠
💐
🌸
💮
🪷
🏵️
🌹
🥀
🌺
🌻
🌼
🌷
🪻
🌱
🪴
🌲
🌳
🌴
🌵
🌾
🌿
☘️
🍀
🍁
🍂
🍃
🪹
🪺
🍄
🪾

View file

@ -0,0 +1,25 @@
:-)
;-)
:-(
:-!
:-$
B-)
=-O
:-P
:O
:-*
:-D
:\'(
:-\\
O:-)
:-[
(╯°
□°)
╯︵
┻━┻
¯\\_
(ツ)
_/¯
┬─┬
︵ /(
.□.\\

View file

@ -0,0 +1,270 @@
🏁
🚩
🎌
🏴
🏳️
🏳️‍🌈
🏳️‍⚧️
🏴‍☠️
🇦🇨
🇦🇩
🇦🇪
🇦🇫
🇦🇬
🇦🇮
🇦🇱
🇦🇲
🇦🇴
🇦🇶
🇦🇷
🇦🇸
🇦🇹
🇦🇺
🇦🇼
🇦🇽
🇦🇿
🇧🇦
🇧🇧
🇧🇩
🇧🇪
🇧🇫
🇧🇬
🇧🇭
🇧🇮
🇧🇯
🇧🇱
🇧🇲
🇧🇳
🇧🇴
🇧🇶
🇧🇷
🇧🇸
🇧🇹
🇧🇻
🇧🇼
🇧🇾
🇧🇿
🇨🇦
🇨🇨
🇨🇩
🇨🇫
🇨🇬
🇨🇭
🇨🇮
🇨🇰
🇨🇱
🇨🇲
🇨🇳
🇨🇴
🇨🇵
🇨🇶
🇨🇷
🇨🇺
🇨🇻
🇨🇼
🇨🇽
🇨🇾
🇨🇿
🇩🇪
🇩🇬
🇩🇯
🇩🇰
🇩🇲
🇩🇴
🇩🇿
🇪🇦
🇪🇨
🇪🇪
🇪🇬
🇪🇭
🇪🇷
🇪🇸
🇪🇹
🇪🇺
🇫🇮
🇫🇯
🇫🇰
🇫🇲
🇫🇴
🇫🇷
🇬🇦
🇬🇧
🇬🇩
🇬🇪
🇬🇫
🇬🇬
🇬🇭
🇬🇮
🇬🇱
🇬🇲
🇬🇳
🇬🇵
🇬🇶
🇬🇷
🇬🇸
🇬🇹
🇬🇺
🇬🇼
🇬🇾
🇭🇰
🇭🇲
🇭🇳
🇭🇷
🇭🇹
🇭🇺
🇮🇨
🇮🇩
🇮🇪
🇮🇱
🇮🇲
🇮🇳
🇮🇴
🇮🇶
🇮🇷
🇮🇸
🇮🇹
🇯🇪
🇯🇲
🇯🇴
🇯🇵
🇰🇪
🇰🇬
🇰🇭
🇰🇮
🇰🇲
🇰🇳
🇰🇵
🇰🇷
🇰🇼
🇰🇾
🇰🇿
🇱🇦
🇱🇧
🇱🇨
🇱🇮
🇱🇰
🇱🇷
🇱🇸
🇱🇹
🇱🇺
🇱🇻
🇱🇾
🇲🇦
🇲🇨
🇲🇩
🇲🇪
🇲🇫
🇲🇬
🇲🇭
🇲🇰
🇲🇱
🇲🇲
🇲🇳
🇲🇴
🇲🇵
🇲🇶
🇲🇷
🇲🇸
🇲🇹
🇲🇺
🇲🇻
🇲🇼
🇲🇽
🇲🇾
🇲🇿
🇳🇦
🇳🇨
🇳🇪
🇳🇫
🇳🇬
🇳🇮
🇳🇱
🇳🇴
🇳🇵
🇳🇷
🇳🇺
🇳🇿
🇴🇲
🇵🇦
🇵🇪
🇵🇫
🇵🇬
🇵🇭
🇵🇰
🇵🇱
🇵🇲
🇵🇳
🇵🇷
🇵🇸
🇵🇹
🇵🇼
🇵🇾
🇶🇦
🇷🇪
🇷🇴
🇷🇸
🇷🇺
🇷🇼
🇸🇦
🇸🇧
🇸🇨
🇸🇩
🇸🇪
🇸🇬
🇸🇭
🇸🇮
🇸🇯
🇸🇰
🇸🇱
🇸🇲
🇸🇳
🇸🇴
🇸🇷
🇸🇸
🇸🇹
🇸🇻
🇸🇽
🇸🇾
🇸🇿
🇹🇦
🇹🇨
🇹🇩
🇹🇫
🇹🇬
🇹🇭
🇹🇯
🇹🇰
🇹🇱
🇹🇲
🇹🇳
🇹🇴
🇹🇷
🇹🇹
🇹🇻
🇹🇼
🇹🇿
🇺🇦
🇺🇬
🇺🇲
🇺🇳
🇺🇸
🇺🇾
🇺🇿
🇻🇦
🇻🇨
🇻🇪
🇻🇬
🇻🇮
🇻🇳
🇻🇺
🇼🇫
🇼🇸
🇽🇰
🇾🇪
🇾🇹
🇿🇦
🇿🇲
🇿🇼
🏴󠁧󠁢󠁥󠁮󠁧󠁿
🏴󠁧󠁢󠁳󠁣󠁴󠁿
🏴󠁧󠁢󠁷󠁬󠁳󠁿

View file

@ -0,0 +1,131 @@
🍇
🍈
🍉
🍊
🍋
🍋‍🟩
🍌
🍍
🥭
🍎
🍏
🍐
🍑
🍒
🍓
🫐
🥝
🍅
🫒
🥥
🥑
🍆
🥔
🥕
🌽
🌶️
🫑
🥒
🥬
🥦
🧄
🧅
🥜
🫘
🌰
🫚
🫛
🍄‍🟫
🫜
🍞
🥐
🥖
🫓
🥨
🥯
🥞
🧇
🧀
🍖
🍗
🥩
🥓
🍔
🍟
🍕
🌭
🥪
🌮
🌯
🫔
🥙
🧆
🥚
🍳
🥘
🍲
🫕
🥣
🥗
🍿
🧈
🧂
🥫
🍱
🍘
🍙
🍚
🍛
🍜
🍝
🍠
🍢
🍣
🍤
🍥
🥮
🍡
🥟
🥠
🥡
🍦
🍧
🍨
🍩
🍪
🎂
🍰
🧁
🥧
🍫
🍬
🍭
🍮
🍯
🍼
🥛
🫖
🍵
🍶
🍾
🍷
🍸
🍹
🍺
🍻
🥂
🥃
🫗
🥤
🧋
🧃
🧉
🧊
🥢
🍽️
🍴
🥄
🔪
🫙
🏺

View file

@ -0,0 +1,264 @@
👓
🕶️
🥽
🥼
🦺
👔
👕
👖
🧣
🧤
🧥
🧦
👗
👘
🥻
🩱
🩲
🩳
👙
👚
🪭
👛
👜
👝
🛍️
🎒
🩴
👞
👟
🥾
🥿
👠
👡
🩰
👢
🪮
👑
👒
🎩
🎓
🧢
🪖
⛑️
📿
💄
💍
💎
🔇
🔈
🔉
🔊
📢
📣
📯
🔔
🔕
🎼
🎵
🎶
🎙️
🎚️
🎛️
🎤
🎧
📻
🎷
🪗
🎸
🎹
🎺
🎻
🪕
🥁
🪘
🪇
🪈
🪉
📱
📲
☎️
📞
📟
📠
🔋
🪫
🔌
💻
🖥️
🖨️
⌨️
🖱️
🖲️
💽
💾
💿
📀
🧮
🎥
🎞️
📽️
🎬
📺
📷
📸
📹
📼
🔍
🔎
🕯️
💡
🔦
🏮
🪔
📔
📕
📖
📗
📘
📙
📚
📓
📒
📃
📜
📄
📰
🗞️
📑
🔖
🏷️
💰
🪙
💴
💵
💶
💷
💸
💳
🧾
💹
✉️
📧
📨
📩
📤
📥
📦
📫
📪
📬
📭
📮
🗳️
✏️
✒️
🖋️
🖊️
🖌️
🖍️
📝
💼
📁
📂
🗂️
📅
📆
🗒️
🗓️
📇
📈
📉
📊
📋
📌
📍
📎
🖇️
📏
📐
✂️
🗃️
🗄️
🗑️
🔒
🔓
🔏
🔐
🔑
🗝️
🔨
🪓
⛏️
⚒️
🛠️
🗡️
⚔️
💣
🪃
🏹
🛡️
🪚
🔧
🪛
🔩
⚙️
🗜️
⚖️
🦯
🔗
⛓️‍💥
⛓️
🪝
🧰
🧲
🪜
🪏
⚗️
🧪
🧫
🧬
🔬
🔭
📡
💉
🩸
💊
🩹
🩼
🩺
🩻
🚪
🛗
🪞
🪟
🛏️
🛋️
🪑
🚽
🪠
🚿
🛁
🪤
🪒
🧴
🧷
🧹
🧺
🧻
🪣
🧼
🫧
🪥
🧽
🧯
🛒
🚬
⚰️
🪦
⚱️
🧿
🪬
🗿
🪧
🪪

View file

@ -0,0 +1,386 @@
👋 👋🏻 👋🏼 👋🏽 👋🏾 👋🏿
🤚 🤚🏻 🤚🏼 🤚🏽 🤚🏾 🤚🏿
🖐️ 🖐🏻 🖐🏼 🖐🏽 🖐🏾 🖐🏿
✋ ✋🏻 ✋🏼 ✋🏽 ✋🏾 ✋🏿
🖖 🖖🏻 🖖🏼 🖖🏽 🖖🏾 🖖🏿
🫱 🫱🏻 🫱🏼 🫱🏽 🫱🏾 🫱🏿
🫲 🫲🏻 🫲🏼 🫲🏽 🫲🏾 🫲🏿
🫳 🫳🏻 🫳🏼 🫳🏽 🫳🏾 🫳🏿
🫴 🫴🏻 🫴🏼 🫴🏽 🫴🏾 🫴🏿
🫷 🫷🏻 🫷🏼 🫷🏽 🫷🏾 🫷🏿
🫸 🫸🏻 🫸🏼 🫸🏽 🫸🏾 🫸🏿
👌 👌🏻 👌🏼 👌🏽 👌🏾 👌🏿
🤌 🤌🏻 🤌🏼 🤌🏽 🤌🏾 🤌🏿
🤏 🤏🏻 🤏🏼 🤏🏽 🤏🏾 🤏🏿
✌️ ✌🏻 ✌🏼 ✌🏽 ✌🏾 ✌🏿
🤞 🤞🏻 🤞🏼 🤞🏽 🤞🏾 🤞🏿
🫰 🫰🏻 🫰🏼 🫰🏽 🫰🏾 🫰🏿
🤟 🤟🏻 🤟🏼 🤟🏽 🤟🏾 🤟🏿
🤘 🤘🏻 🤘🏼 🤘🏽 🤘🏾 🤘🏿
🤙 🤙🏻 🤙🏼 🤙🏽 🤙🏾 🤙🏿
👈 👈🏻 👈🏼 👈🏽 👈🏾 👈🏿
👉 👉🏻 👉🏼 👉🏽 👉🏾 👉🏿
👆 👆🏻 👆🏼 👆🏽 👆🏾 👆🏿
🖕 🖕🏻 🖕🏼 🖕🏽 🖕🏾 🖕🏿
👇 👇🏻 👇🏼 👇🏽 👇🏾 👇🏿
☝️ ☝🏻 ☝🏼 ☝🏽 ☝🏾 ☝🏿
🫵 🫵🏻 🫵🏼 🫵🏽 🫵🏾 🫵🏿
👍 👍🏻 👍🏼 👍🏽 👍🏾 👍🏿
👎 👎🏻 👎🏼 👎🏽 👎🏾 👎🏿
✊ ✊🏻 ✊🏼 ✊🏽 ✊🏾 ✊🏿
👊 👊🏻 👊🏼 👊🏽 👊🏾 👊🏿
🤛 🤛🏻 🤛🏼 🤛🏽 🤛🏾 🤛🏿
🤜 🤜🏻 🤜🏼 🤜🏽 🤜🏾 🤜🏿
👏 👏🏻 👏🏼 👏🏽 👏🏾 👏🏿
🙌 🙌🏻 🙌🏼 🙌🏽 🙌🏾 🙌🏿
🫶 🫶🏻 🫶🏼 🫶🏽 🫶🏾 🫶🏿
👐 👐🏻 👐🏼 👐🏽 👐🏾 👐🏿
🤲 🤲🏻 🤲🏼 🤲🏽 🤲🏾 🤲🏿
🤝 🤝🏻 🤝🏼 🤝🏽 🤝🏾 🤝🏿
🙏 🙏🏻 🙏🏼 🙏🏽 🙏🏾 🙏🏿
✍️ ✍🏻 ✍🏼 ✍🏽 ✍🏾 ✍🏿
💅 💅🏻 💅🏼 💅🏽 💅🏾 💅🏿
🤳 🤳🏻 🤳🏼 🤳🏽 🤳🏾 🤳🏿
💪 💪🏻 💪🏼 💪🏽 💪🏾 💪🏿
🦾
🦿
🦵 🦵🏻 🦵🏼 🦵🏽 🦵🏾 🦵🏿
🦶 🦶🏻 🦶🏼 🦶🏽 🦶🏾 🦶🏿
👂 👂🏻 👂🏼 👂🏽 👂🏾 👂🏿
🦻 🦻🏻 🦻🏼 🦻🏽 🦻🏾 🦻🏿
👃 👃🏻 👃🏼 👃🏽 👃🏾 👃🏿
🧠
🫀
🫁
🦷
🦴
👀
👁️
👅
👄
🫦
👶 👶🏻 👶🏼 👶🏽 👶🏾 👶🏿
🧒 🧒🏻 🧒🏼 🧒🏽 🧒🏾 🧒🏿
👦 👦🏻 👦🏼 👦🏽 👦🏾 👦🏿
👧 👧🏻 👧🏼 👧🏽 👧🏾 👧🏿
🧑 🧑🏻 🧑🏼 🧑🏽 🧑🏾 🧑🏿
👱 👱🏻 👱🏼 👱🏽 👱🏾 👱🏿
👨 👨🏻 👨🏼 👨🏽 👨🏾 👨🏿
🧔 🧔🏻 🧔🏼 🧔🏽 🧔🏾 🧔🏿
🧔‍♂️ 🧔🏻‍♂️ 🧔🏼‍♂️ 🧔🏽‍♂️ 🧔🏾‍♂️ 🧔🏿‍♂️
🧔‍♀️ 🧔🏻‍♀️ 🧔🏼‍♀️ 🧔🏽‍♀️ 🧔🏾‍♀️ 🧔🏿‍♀️
👨‍🦰 👨🏻‍🦰 👨🏼‍🦰 👨🏽‍🦰 👨🏾‍🦰 👨🏿‍🦰
👨‍🦱 👨🏻‍🦱 👨🏼‍🦱 👨🏽‍🦱 👨🏾‍🦱 👨🏿‍🦱
👨‍🦳 👨🏻‍🦳 👨🏼‍🦳 👨🏽‍🦳 👨🏾‍🦳 👨🏿‍🦳
👨‍🦲 👨🏻‍🦲 👨🏼‍🦲 👨🏽‍🦲 👨🏾‍🦲 👨🏿‍🦲
👩 👩🏻 👩🏼 👩🏽 👩🏾 👩🏿
👩‍🦰 👩🏻‍🦰 👩🏼‍🦰 👩🏽‍🦰 👩🏾‍🦰 👩🏿‍🦰
🧑‍🦰 🧑🏻‍🦰 🧑🏼‍🦰 🧑🏽‍🦰 🧑🏾‍🦰 🧑🏿‍🦰
👩‍🦱 👩🏻‍🦱 👩🏼‍🦱 👩🏽‍🦱 👩🏾‍🦱 👩🏿‍🦱
🧑‍🦱 🧑🏻‍🦱 🧑🏼‍🦱 🧑🏽‍🦱 🧑🏾‍🦱 🧑🏿‍🦱
👩‍🦳 👩🏻‍🦳 👩🏼‍🦳 👩🏽‍🦳 👩🏾‍🦳 👩🏿‍🦳
🧑‍🦳 🧑🏻‍🦳 🧑🏼‍🦳 🧑🏽‍🦳 🧑🏾‍🦳 🧑🏿‍🦳
👩‍🦲 👩🏻‍🦲 👩🏼‍🦲 👩🏽‍🦲 👩🏾‍🦲 👩🏿‍🦲
🧑‍🦲 🧑🏻‍🦲 🧑🏼‍🦲 🧑🏽‍🦲 🧑🏾‍🦲 🧑🏿‍🦲
👱‍♀️ 👱🏻‍♀️ 👱🏼‍♀️ 👱🏽‍♀️ 👱🏾‍♀️ 👱🏿‍♀️
👱‍♂️ 👱🏻‍♂️ 👱🏼‍♂️ 👱🏽‍♂️ 👱🏾‍♂️ 👱🏿‍♂️
🧓 🧓🏻 🧓🏼 🧓🏽 🧓🏾 🧓🏿
👴 👴🏻 👴🏼 👴🏽 👴🏾 👴🏿
👵 👵🏻 👵🏼 👵🏽 👵🏾 👵🏿
🙍 🙍🏻 🙍🏼 🙍🏽 🙍🏾 🙍🏿
🙍‍♂️ 🙍🏻‍♂️ 🙍🏼‍♂️ 🙍🏽‍♂️ 🙍🏾‍♂️ 🙍🏿‍♂️
🙍‍♀️ 🙍🏻‍♀️ 🙍🏼‍♀️ 🙍🏽‍♀️ 🙍🏾‍♀️ 🙍🏿‍♀️
🙎 🙎🏻 🙎🏼 🙎🏽 🙎🏾 🙎🏿
🙎‍♂️ 🙎🏻‍♂️ 🙎🏼‍♂️ 🙎🏽‍♂️ 🙎🏾‍♂️ 🙎🏿‍♂️
🙎‍♀️ 🙎🏻‍♀️ 🙎🏼‍♀️ 🙎🏽‍♀️ 🙎🏾‍♀️ 🙎🏿‍♀️
🙅 🙅🏻 🙅🏼 🙅🏽 🙅🏾 🙅🏿
🙅‍♂️ 🙅🏻‍♂️ 🙅🏼‍♂️ 🙅🏽‍♂️ 🙅🏾‍♂️ 🙅🏿‍♂️
🙅‍♀️ 🙅🏻‍♀️ 🙅🏼‍♀️ 🙅🏽‍♀️ 🙅🏾‍♀️ 🙅🏿‍♀️
🙆 🙆🏻 🙆🏼 🙆🏽 🙆🏾 🙆🏿
🙆‍♂️ 🙆🏻‍♂️ 🙆🏼‍♂️ 🙆🏽‍♂️ 🙆🏾‍♂️ 🙆🏿‍♂️
🙆‍♀️ 🙆🏻‍♀️ 🙆🏼‍♀️ 🙆🏽‍♀️ 🙆🏾‍♀️ 🙆🏿‍♀️
💁 💁🏻 💁🏼 💁🏽 💁🏾 💁🏿
💁‍♂️ 💁🏻‍♂️ 💁🏼‍♂️ 💁🏽‍♂️ 💁🏾‍♂️ 💁🏿‍♂️
💁‍♀️ 💁🏻‍♀️ 💁🏼‍♀️ 💁🏽‍♀️ 💁🏾‍♀️ 💁🏿‍♀️
🙋 🙋🏻 🙋🏼 🙋🏽 🙋🏾 🙋🏿
🙋‍♂️ 🙋🏻‍♂️ 🙋🏼‍♂️ 🙋🏽‍♂️ 🙋🏾‍♂️ 🙋🏿‍♂️
🙋‍♀️ 🙋🏻‍♀️ 🙋🏼‍♀️ 🙋🏽‍♀️ 🙋🏾‍♀️ 🙋🏿‍♀️
🧏 🧏🏻 🧏🏼 🧏🏽 🧏🏾 🧏🏿
🧏‍♂️ 🧏🏻‍♂️ 🧏🏼‍♂️ 🧏🏽‍♂️ 🧏🏾‍♂️ 🧏🏿‍♂️
🧏‍♀️ 🧏🏻‍♀️ 🧏🏼‍♀️ 🧏🏽‍♀️ 🧏🏾‍♀️ 🧏🏿‍♀️
🙇 🙇🏻 🙇🏼 🙇🏽 🙇🏾 🙇🏿
🙇‍♂️ 🙇🏻‍♂️ 🙇🏼‍♂️ 🙇🏽‍♂️ 🙇🏾‍♂️ 🙇🏿‍♂️
🙇‍♀️ 🙇🏻‍♀️ 🙇🏼‍♀️ 🙇🏽‍♀️ 🙇🏾‍♀️ 🙇🏿‍♀️
🤦 🤦🏻 🤦🏼 🤦🏽 🤦🏾 🤦🏿
🤦‍♂️ 🤦🏻‍♂️ 🤦🏼‍♂️ 🤦🏽‍♂️ 🤦🏾‍♂️ 🤦🏿‍♂️
🤦‍♀️ 🤦🏻‍♀️ 🤦🏼‍♀️ 🤦🏽‍♀️ 🤦🏾‍♀️ 🤦🏿‍♀️
🤷 🤷🏻 🤷🏼 🤷🏽 🤷🏾 🤷🏿
🤷‍♂️ 🤷🏻‍♂️ 🤷🏼‍♂️ 🤷🏽‍♂️ 🤷🏾‍♂️ 🤷🏿‍♂️
🤷‍♀️ 🤷🏻‍♀️ 🤷🏼‍♀️ 🤷🏽‍♀️ 🤷🏾‍♀️ 🤷🏿‍♀️
🧑‍⚕️ 🧑🏻‍⚕️ 🧑🏼‍⚕️ 🧑🏽‍⚕️ 🧑🏾‍⚕️ 🧑🏿‍⚕️
👨‍⚕️ 👨🏻‍⚕️ 👨🏼‍⚕️ 👨🏽‍⚕️ 👨🏾‍⚕️ 👨🏿‍⚕️
👩‍⚕️ 👩🏻‍⚕️ 👩🏼‍⚕️ 👩🏽‍⚕️ 👩🏾‍⚕️ 👩🏿‍⚕️
🧑‍🎓 🧑🏻‍🎓 🧑🏼‍🎓 🧑🏽‍🎓 🧑🏾‍🎓 🧑🏿‍🎓
👨‍🎓 👨🏻‍🎓 👨🏼‍🎓 👨🏽‍🎓 👨🏾‍🎓 👨🏿‍🎓
👩‍🎓 👩🏻‍🎓 👩🏼‍🎓 👩🏽‍🎓 👩🏾‍🎓 👩🏿‍🎓
🧑‍🏫 🧑🏻‍🏫 🧑🏼‍🏫 🧑🏽‍🏫 🧑🏾‍🏫 🧑🏿‍🏫
👨‍🏫 👨🏻‍🏫 👨🏼‍🏫 👨🏽‍🏫 👨🏾‍🏫 👨🏿‍🏫
👩‍🏫 👩🏻‍🏫 👩🏼‍🏫 👩🏽‍🏫 👩🏾‍🏫 👩🏿‍🏫
🧑‍⚖️ 🧑🏻‍⚖️ 🧑🏼‍⚖️ 🧑🏽‍⚖️ 🧑🏾‍⚖️ 🧑🏿‍⚖️
👨‍⚖️ 👨🏻‍⚖️ 👨🏼‍⚖️ 👨🏽‍⚖️ 👨🏾‍⚖️ 👨🏿‍⚖️
👩‍⚖️ 👩🏻‍⚖️ 👩🏼‍⚖️ 👩🏽‍⚖️ 👩🏾‍⚖️ 👩🏿‍⚖️
🧑‍🌾 🧑🏻‍🌾 🧑🏼‍🌾 🧑🏽‍🌾 🧑🏾‍🌾 🧑🏿‍🌾
👨‍🌾 👨🏻‍🌾 👨🏼‍🌾 👨🏽‍🌾 👨🏾‍🌾 👨🏿‍🌾
👩‍🌾 👩🏻‍🌾 👩🏼‍🌾 👩🏽‍🌾 👩🏾‍🌾 👩🏿‍🌾
🧑‍🍳 🧑🏻‍🍳 🧑🏼‍🍳 🧑🏽‍🍳 🧑🏾‍🍳 🧑🏿‍🍳
👨‍🍳 👨🏻‍🍳 👨🏼‍🍳 👨🏽‍🍳 👨🏾‍🍳 👨🏿‍🍳
👩‍🍳 👩🏻‍🍳 👩🏼‍🍳 👩🏽‍🍳 👩🏾‍🍳 👩🏿‍🍳
🧑‍🔧 🧑🏻‍🔧 🧑🏼‍🔧 🧑🏽‍🔧 🧑🏾‍🔧 🧑🏿‍🔧
👨‍🔧 👨🏻‍🔧 👨🏼‍🔧 👨🏽‍🔧 👨🏾‍🔧 👨🏿‍🔧
👩‍🔧 👩🏻‍🔧 👩🏼‍🔧 👩🏽‍🔧 👩🏾‍🔧 👩🏿‍🔧
🧑‍🏭 🧑🏻‍🏭 🧑🏼‍🏭 🧑🏽‍🏭 🧑🏾‍🏭 🧑🏿‍🏭
👨‍🏭 👨🏻‍🏭 👨🏼‍🏭 👨🏽‍🏭 👨🏾‍🏭 👨🏿‍🏭
👩‍🏭 👩🏻‍🏭 👩🏼‍🏭 👩🏽‍🏭 👩🏾‍🏭 👩🏿‍🏭
🧑‍💼 🧑🏻‍💼 🧑🏼‍💼 🧑🏽‍💼 🧑🏾‍💼 🧑🏿‍💼
👨‍💼 👨🏻‍💼 👨🏼‍💼 👨🏽‍💼 👨🏾‍💼 👨🏿‍💼
👩‍💼 👩🏻‍💼 👩🏼‍💼 👩🏽‍💼 👩🏾‍💼 👩🏿‍💼
🧑‍🔬 🧑🏻‍🔬 🧑🏼‍🔬 🧑🏽‍🔬 🧑🏾‍🔬 🧑🏿‍🔬
👨‍🔬 👨🏻‍🔬 👨🏼‍🔬 👨🏽‍🔬 👨🏾‍🔬 👨🏿‍🔬
👩‍🔬 👩🏻‍🔬 👩🏼‍🔬 👩🏽‍🔬 👩🏾‍🔬 👩🏿‍🔬
🧑‍💻 🧑🏻‍💻 🧑🏼‍💻 🧑🏽‍💻 🧑🏾‍💻 🧑🏿‍💻
👨‍💻 👨🏻‍💻 👨🏼‍💻 👨🏽‍💻 👨🏾‍💻 👨🏿‍💻
👩‍💻 👩🏻‍💻 👩🏼‍💻 👩🏽‍💻 👩🏾‍💻 👩🏿‍💻
🧑‍🎤 🧑🏻‍🎤 🧑🏼‍🎤 🧑🏽‍🎤 🧑🏾‍🎤 🧑🏿‍🎤
👨‍🎤 👨🏻‍🎤 👨🏼‍🎤 👨🏽‍🎤 👨🏾‍🎤 👨🏿‍🎤
👩‍🎤 👩🏻‍🎤 👩🏼‍🎤 👩🏽‍🎤 👩🏾‍🎤 👩🏿‍🎤
🧑‍🎨 🧑🏻‍🎨 🧑🏼‍🎨 🧑🏽‍🎨 🧑🏾‍🎨 🧑🏿‍🎨
👨‍🎨 👨🏻‍🎨 👨🏼‍🎨 👨🏽‍🎨 👨🏾‍🎨 👨🏿‍🎨
👩‍🎨 👩🏻‍🎨 👩🏼‍🎨 👩🏽‍🎨 👩🏾‍🎨 👩🏿‍🎨
🧑‍✈️ 🧑🏻‍✈️ 🧑🏼‍✈️ 🧑🏽‍✈️ 🧑🏾‍✈️ 🧑🏿‍✈️
👨‍✈️ 👨🏻‍✈️ 👨🏼‍✈️ 👨🏽‍✈️ 👨🏾‍✈️ 👨🏿‍✈️
👩‍✈️ 👩🏻‍✈️ 👩🏼‍✈️ 👩🏽‍✈️ 👩🏾‍✈️ 👩🏿‍✈️
🧑‍🚀 🧑🏻‍🚀 🧑🏼‍🚀 🧑🏽‍🚀 🧑🏾‍🚀 🧑🏿‍🚀
👨‍🚀 👨🏻‍🚀 👨🏼‍🚀 👨🏽‍🚀 👨🏾‍🚀 👨🏿‍🚀
👩‍🚀 👩🏻‍🚀 👩🏼‍🚀 👩🏽‍🚀 👩🏾‍🚀 👩🏿‍🚀
🧑‍🚒 🧑🏻‍🚒 🧑🏼‍🚒 🧑🏽‍🚒 🧑🏾‍🚒 🧑🏿‍🚒
👨‍🚒 👨🏻‍🚒 👨🏼‍🚒 👨🏽‍🚒 👨🏾‍🚒 👨🏿‍🚒
👩‍🚒 👩🏻‍🚒 👩🏼‍🚒 👩🏽‍🚒 👩🏾‍🚒 👩🏿‍🚒
👮 👮🏻 👮🏼 👮🏽 👮🏾 👮🏿
👮‍♂️ 👮🏻‍♂️ 👮🏼‍♂️ 👮🏽‍♂️ 👮🏾‍♂️ 👮🏿‍♂️
👮‍♀️ 👮🏻‍♀️ 👮🏼‍♀️ 👮🏽‍♀️ 👮🏾‍♀️ 👮🏿‍♀️
🕵️ 🕵🏻 🕵🏼 🕵🏽 🕵🏾 🕵🏿
🕵️‍♂️ 🕵🏻‍♂️ 🕵🏼‍♂️ 🕵🏽‍♂️ 🕵🏾‍♂️ 🕵🏿‍♂️
🕵️‍♀️ 🕵🏻‍♀️ 🕵🏼‍♀️ 🕵🏽‍♀️ 🕵🏾‍♀️ 🕵🏿‍♀️
💂 💂🏻 💂🏼 💂🏽 💂🏾 💂🏿
💂‍♂️ 💂🏻‍♂️ 💂🏼‍♂️ 💂🏽‍♂️ 💂🏾‍♂️ 💂🏿‍♂️
💂‍♀️ 💂🏻‍♀️ 💂🏼‍♀️ 💂🏽‍♀️ 💂🏾‍♀️ 💂🏿‍♀️
🥷 🥷🏻 🥷🏼 🥷🏽 🥷🏾 🥷🏿
👷 👷🏻 👷🏼 👷🏽 👷🏾 👷🏿
👷‍♂️ 👷🏻‍♂️ 👷🏼‍♂️ 👷🏽‍♂️ 👷🏾‍♂️ 👷🏿‍♂️
👷‍♀️ 👷🏻‍♀️ 👷🏼‍♀️ 👷🏽‍♀️ 👷🏾‍♀️ 👷🏿‍♀️
🫅 🫅🏻 🫅🏼 🫅🏽 🫅🏾 🫅🏿
🤴 🤴🏻 🤴🏼 🤴🏽 🤴🏾 🤴🏿
👸 👸🏻 👸🏼 👸🏽 👸🏾 👸🏿
👳 👳🏻 👳🏼 👳🏽 👳🏾 👳🏿
👳‍♂️ 👳🏻‍♂️ 👳🏼‍♂️ 👳🏽‍♂️ 👳🏾‍♂️ 👳🏿‍♂️
👳‍♀️ 👳🏻‍♀️ 👳🏼‍♀️ 👳🏽‍♀️ 👳🏾‍♀️ 👳🏿‍♀️
👲 👲🏻 👲🏼 👲🏽 👲🏾 👲🏿
🧕 🧕🏻 🧕🏼 🧕🏽 🧕🏾 🧕🏿
🤵 🤵🏻 🤵🏼 🤵🏽 🤵🏾 🤵🏿
🤵‍♂️ 🤵🏻‍♂️ 🤵🏼‍♂️ 🤵🏽‍♂️ 🤵🏾‍♂️ 🤵🏿‍♂️
🤵‍♀️ 🤵🏻‍♀️ 🤵🏼‍♀️ 🤵🏽‍♀️ 🤵🏾‍♀️ 🤵🏿‍♀️
👰 👰🏻 👰🏼 👰🏽 👰🏾 👰🏿
👰‍♂️ 👰🏻‍♂️ 👰🏼‍♂️ 👰🏽‍♂️ 👰🏾‍♂️ 👰🏿‍♂️
👰‍♀️ 👰🏻‍♀️ 👰🏼‍♀️ 👰🏽‍♀️ 👰🏾‍♀️ 👰🏿‍♀️
🤰 🤰🏻 🤰🏼 🤰🏽 🤰🏾 🤰🏿
🫃 🫃🏻 🫃🏼 🫃🏽 🫃🏾 🫃🏿
🫄 🫄🏻 🫄🏼 🫄🏽 🫄🏾 🫄🏿
🤱 🤱🏻 🤱🏼 🤱🏽 🤱🏾 🤱🏿
👩‍🍼 👩🏻‍🍼 👩🏼‍🍼 👩🏽‍🍼 👩🏾‍🍼 👩🏿‍🍼
👨‍🍼 👨🏻‍🍼 👨🏼‍🍼 👨🏽‍🍼 👨🏾‍🍼 👨🏿‍🍼
🧑‍🍼 🧑🏻‍🍼 🧑🏼‍🍼 🧑🏽‍🍼 🧑🏾‍🍼 🧑🏿‍🍼
👼 👼🏻 👼🏼 👼🏽 👼🏾 👼🏿
🎅 🎅🏻 🎅🏼 🎅🏽 🎅🏾 🎅🏿
🤶 🤶🏻 🤶🏼 🤶🏽 🤶🏾 🤶🏿
🧑‍🎄 🧑🏻‍🎄 🧑🏼‍🎄 🧑🏽‍🎄 🧑🏾‍🎄 🧑🏿‍🎄
🦸 🦸🏻 🦸🏼 🦸🏽 🦸🏾 🦸🏿
🦸‍♂️ 🦸🏻‍♂️ 🦸🏼‍♂️ 🦸🏽‍♂️ 🦸🏾‍♂️ 🦸🏿‍♂️
🦸‍♀️ 🦸🏻‍♀️ 🦸🏼‍♀️ 🦸🏽‍♀️ 🦸🏾‍♀️ 🦸🏿‍♀️
🦹 🦹🏻 🦹🏼 🦹🏽 🦹🏾 🦹🏿
🦹‍♂️ 🦹🏻‍♂️ 🦹🏼‍♂️ 🦹🏽‍♂️ 🦹🏾‍♂️ 🦹🏿‍♂️
🦹‍♀️ 🦹🏻‍♀️ 🦹🏼‍♀️ 🦹🏽‍♀️ 🦹🏾‍♀️ 🦹🏿‍♀️
🧙 🧙🏻 🧙🏼 🧙🏽 🧙🏾 🧙🏿
🧙‍♂️ 🧙🏻‍♂️ 🧙🏼‍♂️ 🧙🏽‍♂️ 🧙🏾‍♂️ 🧙🏿‍♂️
🧙‍♀️ 🧙🏻‍♀️ 🧙🏼‍♀️ 🧙🏽‍♀️ 🧙🏾‍♀️ 🧙🏿‍♀️
🧚 🧚🏻 🧚🏼 🧚🏽 🧚🏾 🧚🏿
🧚‍♂️ 🧚🏻‍♂️ 🧚🏼‍♂️ 🧚🏽‍♂️ 🧚🏾‍♂️ 🧚🏿‍♂️
🧚‍♀️ 🧚🏻‍♀️ 🧚🏼‍♀️ 🧚🏽‍♀️ 🧚🏾‍♀️ 🧚🏿‍♀️
🧛 🧛🏻 🧛🏼 🧛🏽 🧛🏾 🧛🏿
🧛‍♂️ 🧛🏻‍♂️ 🧛🏼‍♂️ 🧛🏽‍♂️ 🧛🏾‍♂️ 🧛🏿‍♂️
🧛‍♀️ 🧛🏻‍♀️ 🧛🏼‍♀️ 🧛🏽‍♀️ 🧛🏾‍♀️ 🧛🏿‍♀️
🧜 🧜🏻 🧜🏼 🧜🏽 🧜🏾 🧜🏿
🧜‍♂️ 🧜🏻‍♂️ 🧜🏼‍♂️ 🧜🏽‍♂️ 🧜🏾‍♂️ 🧜🏿‍♂️
🧜‍♀️ 🧜🏻‍♀️ 🧜🏼‍♀️ 🧜🏽‍♀️ 🧜🏾‍♀️ 🧜🏿‍♀️
🧝 🧝🏻 🧝🏼 🧝🏽 🧝🏾 🧝🏿
🧝‍♂️ 🧝🏻‍♂️ 🧝🏼‍♂️ 🧝🏽‍♂️ 🧝🏾‍♂️ 🧝🏿‍♂️
🧝‍♀️ 🧝🏻‍♀️ 🧝🏼‍♀️ 🧝🏽‍♀️ 🧝🏾‍♀️ 🧝🏿‍♀️
🧞
🧞‍♂️
🧞‍♀️
🧟
🧟‍♂️
🧟‍♀️
🧌
💆 💆🏻 💆🏼 💆🏽 💆🏾 💆🏿
💆‍♂️ 💆🏻‍♂️ 💆🏼‍♂️ 💆🏽‍♂️ 💆🏾‍♂️ 💆🏿‍♂️
💆‍♀️ 💆🏻‍♀️ 💆🏼‍♀️ 💆🏽‍♀️ 💆🏾‍♀️ 💆🏿‍♀️
💇 💇🏻 💇🏼 💇🏽 💇🏾 💇🏿
💇‍♂️ 💇🏻‍♂️ 💇🏼‍♂️ 💇🏽‍♂️ 💇🏾‍♂️ 💇🏿‍♂️
💇‍♀️ 💇🏻‍♀️ 💇🏼‍♀️ 💇🏽‍♀️ 💇🏾‍♀️ 💇🏿‍♀️
🚶 🚶🏻 🚶🏼 🚶🏽 🚶🏾 🚶🏿
🚶‍♂️ 🚶🏻‍♂️ 🚶🏼‍♂️ 🚶🏽‍♂️ 🚶🏾‍♂️ 🚶🏿‍♂️
🚶‍♀️ 🚶🏻‍♀️ 🚶🏼‍♀️ 🚶🏽‍♀️ 🚶🏾‍♀️ 🚶🏿‍♀️
🚶‍➡️ 🚶🏻‍➡️ 🚶🏼‍➡️ 🚶🏽‍➡️ 🚶🏾‍➡️ 🚶🏿‍➡️
🚶‍♀️‍➡️ 🚶🏻‍♀️‍➡️ 🚶🏼‍♀️‍➡️ 🚶🏽‍♀️‍➡️ 🚶🏾‍♀️‍➡️ 🚶🏿‍♀️‍➡️
🚶‍♂️‍➡️ 🚶🏻‍♂️‍➡️ 🚶🏼‍♂️‍➡️ 🚶🏽‍♂️‍➡️ 🚶🏾‍♂️‍➡️ 🚶🏿‍♂️‍➡️
🧍 🧍🏻 🧍🏼 🧍🏽 🧍🏾 🧍🏿
🧍‍♂️ 🧍🏻‍♂️ 🧍🏼‍♂️ 🧍🏽‍♂️ 🧍🏾‍♂️ 🧍🏿‍♂️
🧍‍♀️ 🧍🏻‍♀️ 🧍🏼‍♀️ 🧍🏽‍♀️ 🧍🏾‍♀️ 🧍🏿‍♀️
🧎 🧎🏻 🧎🏼 🧎🏽 🧎🏾 🧎🏿
🧎‍♂️ 🧎🏻‍♂️ 🧎🏼‍♂️ 🧎🏽‍♂️ 🧎🏾‍♂️ 🧎🏿‍♂️
🧎‍♀️ 🧎🏻‍♀️ 🧎🏼‍♀️ 🧎🏽‍♀️ 🧎🏾‍♀️ 🧎🏿‍♀️
🧎‍➡️ 🧎🏻‍➡️ 🧎🏼‍➡️ 🧎🏽‍➡️ 🧎🏾‍➡️ 🧎🏿‍➡️
🧎‍♀️‍➡️ 🧎🏻‍♀️‍➡️ 🧎🏼‍♀️‍➡️ 🧎🏽‍♀️‍➡️ 🧎🏾‍♀️‍➡️ 🧎🏿‍♀️‍➡️
🧎‍♂️‍➡️ 🧎🏻‍♂️‍➡️ 🧎🏼‍♂️‍➡️ 🧎🏽‍♂️‍➡️ 🧎🏾‍♂️‍➡️ 🧎🏿‍♂️‍➡️
🧑‍🦯 🧑🏻‍🦯 🧑🏼‍🦯 🧑🏽‍🦯 🧑🏾‍🦯 🧑🏿‍🦯
🧑‍🦯‍➡️ 🧑🏻‍🦯‍➡️ 🧑🏼‍🦯‍➡️ 🧑🏽‍🦯‍➡️ 🧑🏾‍🦯‍➡️ 🧑🏿‍🦯‍➡️
👨‍🦯 👨🏻‍🦯 👨🏼‍🦯 👨🏽‍🦯 👨🏾‍🦯 👨🏿‍🦯
👨‍🦯‍➡️ 👨🏻‍🦯‍➡️ 👨🏼‍🦯‍➡️ 👨🏽‍🦯‍➡️ 👨🏾‍🦯‍➡️ 👨🏿‍🦯‍➡️
👩‍🦯 👩🏻‍🦯 👩🏼‍🦯 👩🏽‍🦯 👩🏾‍🦯 👩🏿‍🦯
👩‍🦯‍➡️ 👩🏻‍🦯‍➡️ 👩🏼‍🦯‍➡️ 👩🏽‍🦯‍➡️ 👩🏾‍🦯‍➡️ 👩🏿‍🦯‍➡️
🧑‍🦼 🧑🏻‍🦼 🧑🏼‍🦼 🧑🏽‍🦼 🧑🏾‍🦼 🧑🏿‍🦼
🧑‍🦼‍➡️ 🧑🏻‍🦼‍➡️ 🧑🏼‍🦼‍➡️ 🧑🏽‍🦼‍➡️ 🧑🏾‍🦼‍➡️ 🧑🏿‍🦼‍➡️
👨‍🦼 👨🏻‍🦼 👨🏼‍🦼 👨🏽‍🦼 👨🏾‍🦼 👨🏿‍🦼
👨‍🦼‍➡️ 👨🏻‍🦼‍➡️ 👨🏼‍🦼‍➡️ 👨🏽‍🦼‍➡️ 👨🏾‍🦼‍➡️ 👨🏿‍🦼‍➡️
👩‍🦼 👩🏻‍🦼 👩🏼‍🦼 👩🏽‍🦼 👩🏾‍🦼 👩🏿‍🦼
👩‍🦼‍➡️ 👩🏻‍🦼‍➡️ 👩🏼‍🦼‍➡️ 👩🏽‍🦼‍➡️ 👩🏾‍🦼‍➡️ 👩🏿‍🦼‍➡️
🧑‍🦽 🧑🏻‍🦽 🧑🏼‍🦽 🧑🏽‍🦽 🧑🏾‍🦽 🧑🏿‍🦽
🧑‍🦽‍➡️ 🧑🏻‍🦽‍➡️ 🧑🏼‍🦽‍➡️ 🧑🏽‍🦽‍➡️ 🧑🏾‍🦽‍➡️ 🧑🏿‍🦽‍➡️
👨‍🦽 👨🏻‍🦽 👨🏼‍🦽 👨🏽‍🦽 👨🏾‍🦽 👨🏿‍🦽
👨‍🦽‍➡️ 👨🏻‍🦽‍➡️ 👨🏼‍🦽‍➡️ 👨🏽‍🦽‍➡️ 👨🏾‍🦽‍➡️ 👨🏿‍🦽‍➡️
👩‍🦽 👩🏻‍🦽 👩🏼‍🦽 👩🏽‍🦽 👩🏾‍🦽 👩🏿‍🦽
👩‍🦽‍➡️ 👩🏻‍🦽‍➡️ 👩🏼‍🦽‍➡️ 👩🏽‍🦽‍➡️ 👩🏾‍🦽‍➡️ 👩🏿‍🦽‍➡️
🏃 🏃🏻 🏃🏼 🏃🏽 🏃🏾 🏃🏿
🏃‍♂️ 🏃🏻‍♂️ 🏃🏼‍♂️ 🏃🏽‍♂️ 🏃🏾‍♂️ 🏃🏿‍♂️
🏃‍♀️ 🏃🏻‍♀️ 🏃🏼‍♀️ 🏃🏽‍♀️ 🏃🏾‍♀️ 🏃🏿‍♀️
🏃‍➡️ 🏃🏻‍➡️ 🏃🏼‍➡️ 🏃🏽‍➡️ 🏃🏾‍➡️ 🏃🏿‍➡️
🏃‍♀️‍➡️ 🏃🏻‍♀️‍➡️ 🏃🏼‍♀️‍➡️ 🏃🏽‍♀️‍➡️ 🏃🏾‍♀️‍➡️ 🏃🏿‍♀️‍➡️
🏃‍♂️‍➡️ 🏃🏻‍♂️‍➡️ 🏃🏼‍♂️‍➡️ 🏃🏽‍♂️‍➡️ 🏃🏾‍♂️‍➡️ 🏃🏿‍♂️‍➡️
💃 💃🏻 💃🏼 💃🏽 💃🏾 💃🏿
🕺 🕺🏻 🕺🏼 🕺🏽 🕺🏾 🕺🏿
🕴️ 🕴🏻 🕴🏼 🕴🏽 🕴🏾 🕴🏿
👯
👯‍♂️
👯‍♀️
🧖 🧖🏻 🧖🏼 🧖🏽 🧖🏾 🧖🏿
🧖‍♂️ 🧖🏻‍♂️ 🧖🏼‍♂️ 🧖🏽‍♂️ 🧖🏾‍♂️ 🧖🏿‍♂️
🧖‍♀️ 🧖🏻‍♀️ 🧖🏼‍♀️ 🧖🏽‍♀️ 🧖🏾‍♀️ 🧖🏿‍♀️
🧗 🧗🏻 🧗🏼 🧗🏽 🧗🏾 🧗🏿
🧗‍♂️ 🧗🏻‍♂️ 🧗🏼‍♂️ 🧗🏽‍♂️ 🧗🏾‍♂️ 🧗🏿‍♂️
🧗‍♀️ 🧗🏻‍♀️ 🧗🏼‍♀️ 🧗🏽‍♀️ 🧗🏾‍♀️ 🧗🏿‍♀️
🤺
🏇 🏇🏻 🏇🏼 🏇🏽 🏇🏾 🏇🏿
⛷️
🏂 🏂🏻 🏂🏼 🏂🏽 🏂🏾 🏂🏿
🏌️ 🏌🏻 🏌🏼 🏌🏽 🏌🏾 🏌🏿
🏌️‍♂️ 🏌🏻‍♂️ 🏌🏼‍♂️ 🏌🏽‍♂️ 🏌🏾‍♂️ 🏌🏿‍♂️
🏌️‍♀️ 🏌🏻‍♀️ 🏌🏼‍♀️ 🏌🏽‍♀️ 🏌🏾‍♀️ 🏌🏿‍♀️
🏄 🏄🏻 🏄🏼 🏄🏽 🏄🏾 🏄🏿
🏄‍♂️ 🏄🏻‍♂️ 🏄🏼‍♂️ 🏄🏽‍♂️ 🏄🏾‍♂️ 🏄🏿‍♂️
🏄‍♀️ 🏄🏻‍♀️ 🏄🏼‍♀️ 🏄🏽‍♀️ 🏄🏾‍♀️ 🏄🏿‍♀️
🚣 🚣🏻 🚣🏼 🚣🏽 🚣🏾 🚣🏿
🚣‍♂️ 🚣🏻‍♂️ 🚣🏼‍♂️ 🚣🏽‍♂️ 🚣🏾‍♂️ 🚣🏿‍♂️
🚣‍♀️ 🚣🏻‍♀️ 🚣🏼‍♀️ 🚣🏽‍♀️ 🚣🏾‍♀️ 🚣🏿‍♀️
🏊 🏊🏻 🏊🏼 🏊🏽 🏊🏾 🏊🏿
🏊‍♂️ 🏊🏻‍♂️ 🏊🏼‍♂️ 🏊🏽‍♂️ 🏊🏾‍♂️ 🏊🏿‍♂️
🏊‍♀️ 🏊🏻‍♀️ 🏊🏼‍♀️ 🏊🏽‍♀️ 🏊🏾‍♀️ 🏊🏿‍♀️
⛹️ ⛹🏻 ⛹🏼 ⛹🏽 ⛹🏾 ⛹🏿
⛹️‍♂️ ⛹🏻‍♂️ ⛹🏼‍♂️ ⛹🏽‍♂️ ⛹🏾‍♂️ ⛹🏿‍♂️
⛹️‍♀️ ⛹🏻‍♀️ ⛹🏼‍♀️ ⛹🏽‍♀️ ⛹🏾‍♀️ ⛹🏿‍♀️
🏋️ 🏋🏻 🏋🏼 🏋🏽 🏋🏾 🏋🏿
🏋️‍♂️ 🏋🏻‍♂️ 🏋🏼‍♂️ 🏋🏽‍♂️ 🏋🏾‍♂️ 🏋🏿‍♂️
🏋️‍♀️ 🏋🏻‍♀️ 🏋🏼‍♀️ 🏋🏽‍♀️ 🏋🏾‍♀️ 🏋🏿‍♀️
🚴 🚴🏻 🚴🏼 🚴🏽 🚴🏾 🚴🏿
🚴‍♂️ 🚴🏻‍♂️ 🚴🏼‍♂️ 🚴🏽‍♂️ 🚴🏾‍♂️ 🚴🏿‍♂️
🚴‍♀️ 🚴🏻‍♀️ 🚴🏼‍♀️ 🚴🏽‍♀️ 🚴🏾‍♀️ 🚴🏿‍♀️
🚵 🚵🏻 🚵🏼 🚵🏽 🚵🏾 🚵🏿
🚵‍♂️ 🚵🏻‍♂️ 🚵🏼‍♂️ 🚵🏽‍♂️ 🚵🏾‍♂️ 🚵🏿‍♂️
🚵‍♀️ 🚵🏻‍♀️ 🚵🏼‍♀️ 🚵🏽‍♀️ 🚵🏾‍♀️ 🚵🏿‍♀️
🤸 🤸🏻 🤸🏼 🤸🏽 🤸🏾 🤸🏿
🤸‍♂️ 🤸🏻‍♂️ 🤸🏼‍♂️ 🤸🏽‍♂️ 🤸🏾‍♂️ 🤸🏿‍♂️
🤸‍♀️ 🤸🏻‍♀️ 🤸🏼‍♀️ 🤸🏽‍♀️ 🤸🏾‍♀️ 🤸🏿‍♀️
🤼
🤼‍♂️
🤼‍♀️
🤽 🤽🏻 🤽🏼 🤽🏽 🤽🏾 🤽🏿
🤽‍♂️ 🤽🏻‍♂️ 🤽🏼‍♂️ 🤽🏽‍♂️ 🤽🏾‍♂️ 🤽🏿‍♂️
🤽‍♀️ 🤽🏻‍♀️ 🤽🏼‍♀️ 🤽🏽‍♀️ 🤽🏾‍♀️ 🤽🏿‍♀️
🤾 🤾🏻 🤾🏼 🤾🏽 🤾🏾 🤾🏿
🤾‍♂️ 🤾🏻‍♂️ 🤾🏼‍♂️ 🤾🏽‍♂️ 🤾🏾‍♂️ 🤾🏿‍♂️
🤾‍♀️ 🤾🏻‍♀️ 🤾🏼‍♀️ 🤾🏽‍♀️ 🤾🏾‍♀️ 🤾🏿‍♀️
🤹 🤹🏻 🤹🏼 🤹🏽 🤹🏾 🤹🏿
🤹‍♂️ 🤹🏻‍♂️ 🤹🏼‍♂️ 🤹🏽‍♂️ 🤹🏾‍♂️ 🤹🏿‍♂️
🤹‍♀️ 🤹🏻‍♀️ 🤹🏼‍♀️ 🤹🏽‍♀️ 🤹🏾‍♀️ 🤹🏿‍♀️
🧘 🧘🏻 🧘🏼 🧘🏽 🧘🏾 🧘🏿
🧘‍♂️ 🧘🏻‍♂️ 🧘🏼‍♂️ 🧘🏽‍♂️ 🧘🏾‍♂️ 🧘🏿‍♂️
🧘‍♀️ 🧘🏻‍♀️ 🧘🏼‍♀️ 🧘🏽‍♀️ 🧘🏾‍♀️ 🧘🏿‍♀️
🛀 🛀🏻 🛀🏼 🛀🏽 🛀🏾 🛀🏿
🛌 🛌🏻 🛌🏼 🛌🏽 🛌🏾 🛌🏿
🧑‍🤝‍🧑 🧑🏻‍🤝‍🧑🏻 🧑🏻‍🤝‍🧑🏼 🧑🏻‍🤝‍🧑🏽 🧑🏻‍🤝‍🧑🏾 🧑🏻‍🤝‍🧑🏿 🧑🏼‍🤝‍🧑🏻 🧑🏼‍🤝‍🧑🏼 🧑🏼‍🤝‍🧑🏽 🧑🏼‍🤝‍🧑🏾 🧑🏼‍🤝‍🧑🏿 🧑🏽‍🤝‍🧑🏻 🧑🏽‍🤝‍🧑🏼 🧑🏽‍🤝‍🧑🏽 🧑🏽‍🤝‍🧑🏾 🧑🏽‍🤝‍🧑🏿 🧑🏾‍🤝‍🧑🏻 🧑🏾‍🤝‍🧑🏼 🧑🏾‍🤝‍🧑🏽 🧑🏾‍🤝‍🧑🏾 🧑🏾‍🤝‍🧑🏿 🧑🏿‍🤝‍🧑🏻 🧑🏿‍🤝‍🧑🏼 🧑🏿‍🤝‍🧑🏽 🧑🏿‍🤝‍🧑🏾 🧑🏿‍🤝‍🧑🏿
👭 👭🏻 👭🏼 👭🏽 👭🏾 👭🏿
👫 👫🏻 👫🏼 👫🏽 👫🏾 👫🏿
👬 👬🏻 👬🏼 👬🏽 👬🏾 👬🏿
💏 💏🏻 💏🏼 💏🏽 💏🏾 💏🏿
👩‍❤️‍💋‍👨 👩🏻‍❤️‍💋‍👨🏻 👩🏻‍❤️‍💋‍👨🏼 👩🏻‍❤️‍💋‍👨🏽 👩🏻‍❤️‍💋‍👨🏾 👩🏻‍❤️‍💋‍👨🏿 👩🏼‍❤️‍💋‍👨🏻 👩🏼‍❤️‍💋‍👨🏼 👩🏼‍❤️‍💋‍👨🏽 👩🏼‍❤️‍💋‍👨🏾 👩🏼‍❤️‍💋‍👨🏿 👩🏽‍❤️‍💋‍👨🏻 👩🏽‍❤️‍💋‍👨🏼 👩🏽‍❤️‍💋‍👨🏽 👩🏽‍❤️‍💋‍👨🏾 👩🏽‍❤️‍💋‍👨🏿 👩🏾‍❤️‍💋‍👨🏻 👩🏾‍❤️‍💋‍👨🏼 👩🏾‍❤️‍💋‍👨🏽 👩🏾‍❤️‍💋‍👨🏾 👩🏾‍❤️‍💋‍👨🏿 👩🏿‍❤️‍💋‍👨🏻 👩🏿‍❤️‍💋‍👨🏼 👩🏿‍❤️‍💋‍👨🏽 👩🏿‍❤️‍💋‍👨🏾 👩🏿‍❤️‍💋‍👨🏿
👨‍❤️‍💋‍👨 👨🏻‍❤️‍💋‍👨🏻 👨🏻‍❤️‍💋‍👨🏼 👨🏻‍❤️‍💋‍👨🏽 👨🏻‍❤️‍💋‍👨🏾 👨🏻‍❤️‍💋‍👨🏿 👨🏼‍❤️‍💋‍👨🏻 👨🏼‍❤️‍💋‍👨🏼 👨🏼‍❤️‍💋‍👨🏽 👨🏼‍❤️‍💋‍👨🏾 👨🏼‍❤️‍💋‍👨🏿 👨🏽‍❤️‍💋‍👨🏻 👨🏽‍❤️‍💋‍👨🏼 👨🏽‍❤️‍💋‍👨🏽 👨🏽‍❤️‍💋‍👨🏾 👨🏽‍❤️‍💋‍👨🏿 👨🏾‍❤️‍💋‍👨🏻 👨🏾‍❤️‍💋‍👨🏼 👨🏾‍❤️‍💋‍👨🏽 👨🏾‍❤️‍💋‍👨🏾 👨🏾‍❤️‍💋‍👨🏿 👨🏿‍❤️‍💋‍👨🏻 👨🏿‍❤️‍💋‍👨🏼 👨🏿‍❤️‍💋‍👨🏽 👨🏿‍❤️‍💋‍👨🏾 👨🏿‍❤️‍💋‍👨🏿
👩‍❤️‍💋‍👩 👩🏻‍❤️‍💋‍👩🏻 👩🏻‍❤️‍💋‍👩🏼 👩🏻‍❤️‍💋‍👩🏽 👩🏻‍❤️‍💋‍👩🏾 👩🏻‍❤️‍💋‍👩🏿 👩🏼‍❤️‍💋‍👩🏻 👩🏼‍❤️‍💋‍👩🏼 👩🏼‍❤️‍💋‍👩🏽 👩🏼‍❤️‍💋‍👩🏾 👩🏼‍❤️‍💋‍👩🏿 👩🏽‍❤️‍💋‍👩🏻 👩🏽‍❤️‍💋‍👩🏼 👩🏽‍❤️‍💋‍👩🏽 👩🏽‍❤️‍💋‍👩🏾 👩🏽‍❤️‍💋‍👩🏿 👩🏾‍❤️‍💋‍👩🏻 👩🏾‍❤️‍💋‍👩🏼 👩🏾‍❤️‍💋‍👩🏽 👩🏾‍❤️‍💋‍👩🏾 👩🏾‍❤️‍💋‍👩🏿 👩🏿‍❤️‍💋‍👩🏻 👩🏿‍❤️‍💋‍👩🏼 👩🏿‍❤️‍💋‍👩🏽 👩🏿‍❤️‍💋‍👩🏾 👩🏿‍❤️‍💋‍👩🏿
💑 💑🏻 💑🏼 💑🏽 💑🏾 💑🏿
👩‍❤️‍👨 👩🏻‍❤️‍👨🏻 👩🏻‍❤️‍👨🏼 👩🏻‍❤️‍👨🏽 👩🏻‍❤️‍👨🏾 👩🏻‍❤️‍👨🏿 👩🏼‍❤️‍👨🏻 👩🏼‍❤️‍👨🏼 👩🏼‍❤️‍👨🏽 👩🏼‍❤️‍👨🏾 👩🏼‍❤️‍👨🏿 👩🏽‍❤️‍👨🏻 👩🏽‍❤️‍👨🏼 👩🏽‍❤️‍👨🏽 👩🏽‍❤️‍👨🏾 👩🏽‍❤️‍👨🏿 👩🏾‍❤️‍👨🏻 👩🏾‍❤️‍👨🏼 👩🏾‍❤️‍👨🏽 👩🏾‍❤️‍👨🏾 👩🏾‍❤️‍👨🏿 👩🏿‍❤️‍👨🏻 👩🏿‍❤️‍👨🏼 👩🏿‍❤️‍👨🏽 👩🏿‍❤️‍👨🏾 👩🏿‍❤️‍👨🏿
👨‍❤️‍👨 👨🏻‍❤️‍👨🏻 👨🏻‍❤️‍👨🏼 👨🏻‍❤️‍👨🏽 👨🏻‍❤️‍👨🏾 👨🏻‍❤️‍👨🏿 👨🏼‍❤️‍👨🏻 👨🏼‍❤️‍👨🏼 👨🏼‍❤️‍👨🏽 👨🏼‍❤️‍👨🏾 👨🏼‍❤️‍👨🏿 👨🏽‍❤️‍👨🏻 👨🏽‍❤️‍👨🏼 👨🏽‍❤️‍👨🏽 👨🏽‍❤️‍👨🏾 👨🏽‍❤️‍👨🏿 👨🏾‍❤️‍👨🏻 👨🏾‍❤️‍👨🏼 👨🏾‍❤️‍👨🏽 👨🏾‍❤️‍👨🏾 👨🏾‍❤️‍👨🏿 👨🏿‍❤️‍👨🏻 👨🏿‍❤️‍👨🏼 👨🏿‍❤️‍👨🏽 👨🏿‍❤️‍👨🏾 👨🏿‍❤️‍👨🏿
👩‍❤️‍👩 👩🏻‍❤️‍👩🏻 👩🏻‍❤️‍👩🏼 👩🏻‍❤️‍👩🏽 👩🏻‍❤️‍👩🏾 👩🏻‍❤️‍👩🏿 👩🏼‍❤️‍👩🏻 👩🏼‍❤️‍👩🏼 👩🏼‍❤️‍👩🏽 👩🏼‍❤️‍👩🏾 👩🏼‍❤️‍👩🏿 👩🏽‍❤️‍👩🏻 👩🏽‍❤️‍👩🏼 👩🏽‍❤️‍👩🏽 👩🏽‍❤️‍👩🏾 👩🏽‍❤️‍👩🏿 👩🏾‍❤️‍👩🏻 👩🏾‍❤️‍👩🏼 👩🏾‍❤️‍👩🏽 👩🏾‍❤️‍👩🏾 👩🏾‍❤️‍👩🏿 👩🏿‍❤️‍👩🏻 👩🏿‍❤️‍👩🏼 👩🏿‍❤️‍👩🏽 👩🏿‍❤️‍👩🏾 👩🏿‍❤️‍👩🏿
👨‍👩‍👦
👨‍👩‍👧
👨‍👩‍👧‍👦
👨‍👩‍👦‍👦
👨‍👩‍👧‍👧
👨‍👨‍👦
👨‍👨‍👧
👨‍👨‍👧‍👦
👨‍👨‍👦‍👦
👨‍👨‍👧‍👧
👩‍👩‍👦
👩‍👩‍👧
👩‍👩‍👧‍👦
👩‍👩‍👦‍👦
👩‍👩‍👧‍👧
👨‍👦
👨‍👦‍👦
👨‍👧
👨‍👧‍👦
👨‍👧‍👧
👩‍👦
👩‍👦‍👦
👩‍👧
👩‍👧‍👦
👩‍👧‍👧
🗣️
👤
👥
🫂
👪
🧑‍🧑‍🧒
🧑‍🧑‍🧒‍🧒
🧑‍🧒
🧑‍🧒‍🧒
👣
🫆

View file

@ -0,0 +1,169 @@
😀
😃
😄
😁
😆
😅
🤣
😂
🙂
🙃
🫠
😉
😊
😇
🥰
😍
🤩
😘
😗
☺️
😚
😙
🥲
😋
😛
😜
🤪
😝
🤑
🤗
🤭
🫢
🫣
🤫
🤔
🫡
🤐
🤨
😐
😑
😶
🫥
😶‍🌫️
😏
😒
🙄
😬
😮‍💨
🤥
🫨
🙂‍↔️
🙂‍↕️
😌
😔
😪
🤤
😴
🫩
😷
🤒
🤕
🤢
🤮
🤧
🥵
🥶
🥴
😵
😵‍💫
🤯
🤠
🥳
🥸
😎
🤓
🧐
😕
🫤
😟
🙁
☹️
😮
😯
😲
😳
🥺
🥹
😦
😧
😨
😰
😥
😢
😭
😱
😖
😣
😞
😓
😩
😫
🥱
😤
😡
😠
🤬
😈
👿
💀
☠️
💩
🤡
👹
👺
👻
👽
👾
🤖
😺
😸
😹
😻
😼
😽
🙀
😿
😾
🙈
🙉
🙊
💌
💘
💝
💖
💗
💓
💞
💕
💟
❣️
💔
❤️‍🔥
❤️‍🩹
❤️
🩷
🧡
💛
💚
💙
🩵
💜
🤎
🖤
🩶
🤍
💋
💯
💢
💥
💫
💦
💨
🕳️
💬
👁️‍🗨️
🗨️
🗯️
💭
💤

View file

@ -0,0 +1,250 @@
🏧
🚮
🚰
🚹
🚺
🚻
🚼
🚾
🛂
🛃
🛄
🛅
⚠️
🚸
🚫
🚳
🚭
🚯
🚱
🚷
📵
🔞
☢️
☣️
⬆️
↗️
➡️
↘️
⬇️
↙️
⬅️
↖️
↕️
↔️
↩️
↪️
⤴️
⤵️
🔃
🔄
🔙
🔚
🔛
🔜
🔝
🛐
⚛️
🕉️
✡️
☸️
☯️
✝️
☦️
☪️
☮️
🕎
🔯
🪯
🔀
🔁
🔂
▶️
⏭️
⏯️
◀️
⏮️
🔼
🔽
⏸️
⏹️
⏺️
⏏️
🎦
🔅
🔆
📶
🛜
📳
📴
♀️
♂️
⚧️
✖️
🟰
♾️
‼️
⁉️
〰️
💱
💲
⚕️
♻️
⚜️
🔱
📛
🔰
☑️
✔️
〽️
✳️
✴️
❇️
©️
®️
™️
🫟
🇦
🇧
🇨
🇩
🇪
🇫
🇬
🇭
🇮
🇯
🇰
🇱
🇲
🇳
🇴
🇵
🇶
🇷
🇸
🇹
🇺
🇻
🇼
🇽
🇾
🇿
#️⃣
*️⃣
0
1
2
3
4
5
6
7
8
9
🔟
🔠
🔡
🔢
🔣
🔤
🅰️
🆎
🅱️
🆑
🆒
🆓
🆔
Ⓜ️
🆕
🆖
🅾️
🆗
🅿️
🆘
🆙
🆚
🈁
🈂️
🈷️
🈶
🈯
🉐
🈹
🈚
🈲
🉑
🈸
🈴
🈳
㊗️
㊙️
🈺
🈵
🔴
🟠
🟡
🟢
🔵
🟣
🟤
🟥
🟧
🟨
🟩
🟦
🟪
🟫
◼️
◻️
▪️
▫️
🔶
🔷
🔸
🔹
🔺
🔻
💠
🔘
🔳
🔲

View file

@ -0,0 +1,218 @@
🌍
🌎
🌏
🌐
🗺️
🗾
🧭
🏔️
⛰️
🌋
🗻
🏕️
🏖️
🏜️
🏝️
🏞️
🏟️
🏛️
🏗️
🧱
🪨
🪵
🛖
🏘️
🏚️
🏠
🏡
🏢
🏣
🏤
🏥
🏦
🏨
🏩
🏪
🏫
🏬
🏭
🏯
🏰
💒
🗼
🗽
🕌
🛕
🕍
⛩️
🕋
🌁
🌃
🏙️
🌄
🌅
🌆
🌇
🌉
♨️
🎠
🛝
🎡
🎢
💈
🎪
🚂
🚃
🚄
🚅
🚆
🚇
🚈
🚉
🚊
🚝
🚞
🚋
🚌
🚍
🚎
🚐
🚑
🚒
🚓
🚔
🚕
🚖
🚗
🚘
🚙
🛻
🚚
🚛
🚜
🏎️
🏍️
🛵
🦽
🦼
🛺
🚲
🛴
🛹
🛼
🚏
🛣️
🛤️
🛢️
🛞
🚨
🚥
🚦
🛑
🚧
🛟
🛶
🚤
🛳️
⛴️
🛥️
🚢
✈️
🛩️
🛫
🛬
🪂
💺
🚁
🚟
🚠
🚡
🛰️
🚀
🛸
🛎️
🧳
⏱️
⏲️
🕰️
🕛
🕧
🕐
🕜
🕑
🕝
🕒
🕞
🕓
🕟
🕔
🕠
🕕
🕡
🕖
🕢
🕗
🕣
🕘
🕤
🕙
🕥
🕚
🕦
🌑
🌒
🌓
🌔
🌕
🌖
🌗
🌘
🌙
🌚
🌛
🌜
🌡️
☀️
🌝
🌞
🪐
🌟
🌠
🌌
☁️
⛈️
🌤️
🌥️
🌦️
🌧️
🌨️
🌩️
🌪️
🌫️
🌬️
🌀
🌈
🌂
☂️
⛱️
❄️
☃️
☄️
🔥
💧
🌊

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,28 @@
ɛ q
w
e
r
t
y
u
i
o
p
a
s
d
f
g
h
j
k
l
z
ɔ x
c ¢
v
b
n
m

View file

@ -0,0 +1,29 @@
q
w
e
r
t
y
u
i
o
p
ŋ
a
s
d
f
g
h
j
k
l
z
x
c
v
b
n
m

View file

@ -0,0 +1,31 @@
ق
و
ە
ر
ت
ی
ێ
ئ
ۆ
پ
ا
س
ش
د
ف
ھ|ه
ژ
ل
ک
گ
ز
ع
ح
ج
چ
خ
ب
ن
م

View file

@ -0,0 +1,28 @@
q
w
ɛ e
r ¢
t
y
u
i
ɔ o
p
a
s
d
f
ɣ g
h
j
k
l
ʒ z
x x
c
v
b
ŋ n
m

View file

@ -0,0 +1,28 @@
ɛ q
w
e
r
t
ɣ y
u
i
o
p
a
s
d
f
g
h
j
k
l
z
ɔ x
c ¢
v
b
n
m

View file

@ -0,0 +1,28 @@
ɛ q
w
e
r
t
y
u
i
o
p
a
s
d
f
g
h
j
k
l
z
ɔ x
ŋ c ¢
v
b
n
m

View file

@ -0,0 +1,28 @@
ẹ q
w
e
r
t
y
u
i
o
p
a
s
d
f
g
h
j
k
l
z
ọ x
c
v
b
n ₦
m

View file

@ -0,0 +1,28 @@
ṅ q
w
e
r
t
y
u
i
o
p
a
s
d
f
g
h
j
k
l
z
ọ x
c
ụ v
b
n ₦
m

View file

@ -0,0 +1,28 @@
ĩ q
w
e
r
t
y
u
i
o
p
a
s
d
f
g
h
j
k
l
z
ũ x
c
v
b
n
m

View file

@ -0,0 +1,55 @@
[
[
{ "label": "\u3147" },
{ "label": "\u3161" },
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "\u3156" },
"default": { "label": "\u3154", "popup": { "main": { "label": "\u3156" } } }
},
{ "label": "\u3139" },
{ "label": "\u314c" },
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "\u3152" },
"default": { "label": "\u3150", "popup": { "main": { "label": "\u3152" } } }
},
{ "label": "\u315c" },
{ "label": "\u3163" },
{ "label": "\u3157" },
{ "label": "\u314d" }
],
[
{ "label": "\u314f" },
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "\u3146" },
"default": { "label": "\u3145", "popup": { "main": { "label": "\u3146" } } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "\u3138" },
"default": { "label": "\u3137", "popup": { "main": { "label": "\u3138" } } }
},
{ "label": "\u3151" },
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "\u3132" },
"default": { "label": "\u3131", "popup": { "main": { "label": "\u3132" } } }
},
{ "label": "\u314e" },
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "\u3149" },
"default": { "label": "\u3148", "popup": { "main": { "label": "\u3149" } } }
},
{ "label": "\u314b" },
{ "label": "\u315b" }
],
[
{ "label": "\u3155" },
{ "label": "\u3160" },
{ "label": "\u314a" },
{ "label": "\u3153" },
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "\u3143" },
"default": { "label": "\u3142", "popup": { "main": { "label": "\u3143" } } }
},
{ "label": "\u3134" },
{ "label": "\u3141" }
]
]

View file

@ -0,0 +1,28 @@
q
w
ɛ e
r
t
y
u
i
ɔ o
p
a
s
d
f
g
h
j
k
l
z
x
c
̌ v
b
n
m

View file

@ -0,0 +1,29 @@
q
w
e
r
t
y
u
i
o
p
ŋ
a
s
d
f
g
h
j
k
l
z
x
c
v
b
n
m

View file

@ -8,7 +8,7 @@
ш
щ
з
х
х ъ [ {
ф
ы
@ -20,7 +20,7 @@
л
д
ж
э
э э́ ] }
я
ч

View file

@ -0,0 +1,34 @@
й
ц
у
к
е
н
г
ш
щ
з
х [ {
ъ ] }
ф
ы
в
а
п
р
о
л
д
ж
э э́
я
ч
с
м
и
т
ь
б <
ю >

View file

@ -0,0 +1,28 @@
q
w
e
r
t
y
u
i
o
p
a
š s
d
f
g
h
j
k
l
z
x
c
v
b
n
m

View file

@ -8,7 +8,8 @@
ш
щ
з
х
х [ {
ї ] }
ф
і
@ -20,7 +21,7 @@
л
д
ж
є
є ' "
я
ч
@ -30,4 +31,4 @@
т
ь
б <
ю >
ю > ґ

View file

@ -0,0 +1,35 @@
й
ц
у
к
е
н
г
ш
щ
з
х [ {
ї ] }
ф
і
в
а
п
р
о
л
д
ж
є ' "
' "
я
ч
с
м
и
т
ь
б <
ю > ґ

View file

@ -0,0 +1,28 @@
ẹ q
w
e
r
t
y
u
i
o
p
a
s
d
f
g
h
j
k
l
z
ọ x
c
ṣ v
b
n ₦
m

View file

@ -0,0 +1,44 @@
[
[
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "!" },
"default": { "label": "1", "popup": { "relevant": [{ "label": "¹" }, { "label": "½" }, { "label": "⅓" }, { "label": "¼" }, { "label": "⅛" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "@" },
"default": { "label": "2", "popup": { "relevant": [{ "label": "²" }, { "label": "⅔" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "#" },
"default": { "label": "3", "popup": { "relevant": [{ "label": "³" }, { "label": "¾" }, { "label": "⅜" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "$" },
"default": { "label": "4", "popup": { "relevant": [{ "label": "⁴" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "%" },
"default": { "label": "5", "popup": { "relevant": [{ "label": "⁵" }, { "label": "⅝" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "^" },
"default": { "label": "6", "popup": { "relevant": [{ "label": "⁶" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "&" },
"default": { "label": "7", "popup": { "relevant": [{ "label": "⁷" }, { "label": "⅞" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "*" },
"default": { "label": "8", "popup": { "relevant": [{ "label": "⁸" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": "(" },
"default": { "label": "9", "popup": { "relevant": [{ "label": "⁹" }] } }
},
{ "$": "shift_state_selector",
"manualOrLocked": { "label": ")" },
"default": { "label": "0", "popup": { "relevant": [{ "label": "⁰" }, { "label": "ⁿ" }, { "label": "∅" }] } }
}
]
]

View file

@ -7,4 +7,4 @@
7 ⁷ ⅞
8 ⁸
9 ⁹
0 ⁰ ⁿ ∅
0 ⁰ ⁿ ∅

View file

@ -0,0 +1,39 @@
[popup_keys]
ق ٯ
و وو
ە ة ـہ
ر ڕ ڒ ࢪ
ت ط
ی ي ې ۍ
ێ ؽ
ئ ء ﺋ
ۆ ؤ ۏ ۊ ۋ ۉ ۇ
پ ث
ا أ إ آ ٱ
س ص
ش ض
د ۮ ڌ ﮆ
ف ڤ ڡ
ھ ھ
ژ ━|ـ
ل ڵ
ک ك ڪ
گ غ
ز ظ
ع ؏
ب ى
punctuation !autoColumnOrder!8 \؟ ! ، ٫ ؍ : ؛ ; : | - @ _ # * ٪ & ^
« „ “ ”
»
[labels]
alphabet: ئ‌پ‌گ
symbol: ٣٢١؟
comma: ،
question: ؟
[number_row]
١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٠
[tlds]
iq krd

View file

@ -1,9 +1,19 @@
[popup_keys]
е ё
ь ъ
е ё е́ ѣ
ф ѳ
ы ы́
а а́
о о́
я я́
и и́
ь ъ ы
ю ю́
'
" ” „ “ » «
і ы
є э э́
[labels]
alphabet: АБВ

View file

@ -1,9 +1,19 @@
[popup_keys]
е е́
г ґ
ь
ф ѳ
і ї
'
" ” „ “
а а́
о о́
я я́
и и́ і ї
г ґ
ю ю́
'
" ” „ “ » «
ы і ї
э є
[labels]
alphabet: АБВ

View file

@ -14,7 +14,7 @@ import android.view.MotionEvent
import helium314.keyboard.accessibility.AccessibilityLongPressTimer.LongPressTimerCallback
import helium314.keyboard.keyboard.*
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.SubtypeLocaleUtils.displayName
/**
* This class represents a delegate that can be registered in [MainKeyboardView] to enhance
@ -86,9 +86,7 @@ class MainKeyboardAccessibilityDelegate(
* @param keyboard The new keyboard.
*/
private fun announceKeyboardLanguage(keyboard: Keyboard) {
val languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
keyboard.mId.mSubtype.rawSubtype)
sendWindowStateChanged(languageText)
sendWindowStateChanged(keyboard.mId.mSubtype.rawSubtype.displayName())
}
/**

View file

@ -24,6 +24,7 @@ import helium314.keyboard.latin.common.StringUtils;
import helium314.keyboard.latin.utils.PopupKeysUtilsKt;
import helium314.keyboard.latin.utils.ToolbarKey;
import helium314.keyboard.latin.utils.ToolbarUtilsKt;
import kotlin.collections.ArraysKt;
import java.util.Arrays;
import java.util.Locale;
@ -919,7 +920,7 @@ public class Key implements Comparable<Key> {
@NonNull final Drawable spacebarBackground,
@NonNull final Drawable actionKeyBackground) {
final Drawable background;
if (isAccentColored()) {
if (hasActionKeyBackground()) {
background = actionKeyBackground;
} else if (hasFunctionalBackground()) {
background = functionalKeyBackground;
@ -933,17 +934,10 @@ public class Key implements Comparable<Key> {
return background;
}
public final boolean isAccentColored() {
if (hasActionKeyBackground()) return true;
final String iconName = getIconName();
if (iconName == null) return false;
// todo: other way of identifying the color?
// this should be done differently, as users can set any icon now
// how is the background drawable selected? can we use the same way?
return iconName.equals(KeyboardIconsSet.NAME_NEXT_KEY)
|| iconName.equals(KeyboardIconsSet.NAME_PREVIOUS_KEY)
|| iconName.equals("clipboard_action_key")
|| iconName.equals("emoji_action_key");
public final boolean hasActionKeyPopups() {
if (!hasActionKeyBackground()) return false;
// only use the special action key popups for action colored keys, and only for icon popups
return ArraysKt.none(getPopupKeys(), (key) -> key.mIconName == null);
}
public boolean hasFunctionalBackground() {

View file

@ -1,5 +1,6 @@
package helium314.keyboard.keyboard
import android.text.InputType
import android.view.KeyEvent
import android.view.inputmethod.InputMethodSubtype
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
@ -13,9 +14,12 @@ import helium314.keyboard.latin.common.loopOverCodePointsBackwards
import helium314.keyboard.latin.inputlogic.InputLogic
import helium314.keyboard.latin.settings.Settings
import kotlin.math.abs
import kotlin.math.min
class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inputLogic: InputLogic) : KeyboardActionListener {
private val connection = inputLogic.mConnection
private val keyboardSwitcher = KeyboardSwitcher.getInstance()
private val settings = Settings.getInstance()
private var metaState = 0 // is this enough, or are there threading issues with the different PointerTrackers?
@ -28,9 +32,15 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
private fun adjustMetaState(code: Int, remove: Boolean) {
val metaCode = when (code) {
KeyCode.CTRL -> KeyEvent.META_CTRL_ON
KeyCode.CTRL_LEFT -> KeyEvent.META_CTRL_LEFT_ON
KeyCode.CTRL_RIGHT -> KeyEvent.META_CTRL_RIGHT_ON
KeyCode.ALT -> KeyEvent.META_ALT_ON
KeyCode.ALT_LEFT -> KeyEvent.META_ALT_LEFT_ON
KeyCode.ALT_RIGHT -> KeyEvent.META_ALT_RIGHT_ON
KeyCode.FN -> KeyEvent.META_FUNCTION_ON
KeyCode.META -> KeyEvent.META_META_ON
KeyCode.META_LEFT -> KeyEvent.META_META_LEFT_ON
KeyCode.META_RIGHT -> KeyEvent.META_META_RIGHT_ON
else -> return
}
metaState = if (remove) metaState and metaCode.inv()
@ -70,8 +80,9 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
keyboardSwitcher.onFinishSlidingInput(latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
override fun onCustomRequest(requestCode: Int): Boolean {
if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)
if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER) {
return latinIME.showInputPickerDialog()
}
return false
}
@ -101,30 +112,34 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
override fun onMoveDeletePointer(steps: Int) {
inputLogic.finishInput()
val end = inputLogic.mConnection.expectedSelectionEnd
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
val end = connection.expectedSelectionEnd
val actualSteps = actualSteps(steps)
val start = connection.expectedSelectionStart + actualSteps
if (start > end) return
connection.setSelection(start, end)
}
private fun actualSteps(steps: Int): Int {
var actualSteps = 0
// corrected steps to avoid splitting chars belonging to the same codepoint
if (steps > 0) {
val text = inputLogic.mConnection.getSelectedText(0)
if (text == null) actualSteps = steps
else loopOverCodePoints(text) {
actualSteps += Character.charCount(it)
val text = connection.getSelectedText(0) ?: return steps
loopOverCodePoints(text) { cp, charCount ->
actualSteps += charCount
actualSteps >= steps
}
} else {
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0)
if (text == null) actualSteps = steps
else loopOverCodePointsBackwards(text) {
actualSteps -= Character.charCount(it)
val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return steps
loopOverCodePointsBackwards(text) { cp, charCount ->
actualSteps -= charCount
actualSteps <= steps
}
}
val start = inputLogic.mConnection.expectedSelectionStart + actualSteps
if (start > end) return
inputLogic.mConnection.setSelection(start, end)
return actualSteps
}
override fun onUpWithDeletePointerActive() {
if (!inputLogic.mConnection.hasSelection()) return
if (!connection.hasSelection()) return
inputLogic.finishInput()
onCodeInput(KeyCode.DELETE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
}
@ -143,16 +158,17 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
val current = RichInputMethodManager.getInstance().currentSubtype.rawSubtype
var wantedIndex = subtypes.indexOf(current) + if (steps > 0) 1 else -1
wantedIndex %= subtypes.size
if (wantedIndex < 0)
if (wantedIndex < 0) {
wantedIndex += subtypes.size
}
val newSubtype = subtypes[wantedIndex]
// do not switch if we would switch to the initial subtype after cycling all other subtypes
if (initialSubtype == null)
initialSubtype = current
if (initialSubtype == null) initialSubtype = current
if (initialSubtype == newSubtype) {
if ((subtypeSwitchCount > 0 && steps > 0) || ((subtypeSwitchCount < 0 && steps < 0)))
if ((subtypeSwitchCount > 0 && steps > 0) || (subtypeSwitchCount < 0 && steps < 0)) {
return true
}
}
if (steps > 0) subtypeSwitchCount++ else subtypeSwitchCount--
@ -173,17 +189,8 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
val steps = if (RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype) -rawSteps else rawSteps
val moveSteps: Int
if (steps < 0) {
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0) ?: return false
loopOverCodePointsBackwards(text) {
if (StringUtils.mightBeEmoji(it)) {
actualSteps = 0
return@loopOverCodePointsBackwards true
}
actualSteps -= Character.charCount(it)
actualSteps <= steps
}
moveSteps = -text.length.coerceAtMost(abs(actualSteps))
val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return false
moveSteps = negativeMoveSteps(text, steps)
if (moveSteps == 0) {
// some apps don't return any text via input connection, and the cursor can't be moved
// we fall back to virtually pressing the left/right key one or more times instead
@ -193,36 +200,61 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
return true
}
} else {
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
val text = inputLogic.mConnection.getTextAfterCursor(steps * 4, 0) ?: return false
loopOverCodePoints(text) {
if (StringUtils.mightBeEmoji(it)) {
actualSteps = 0
return@loopOverCodePoints true
}
actualSteps += Character.charCount(it)
actualSteps >= steps
}
moveSteps = text.length.coerceAtMost(actualSteps)
val text = connection.getTextAfterCursor(steps * 4, 0) ?: return false
moveSteps = positiveMoveSteps(text, steps)
if (moveSteps == 0) {
// some apps don't return any text via input connection, and the cursor can't be moved
// we fall back to virtually pressing the left/right key one or more times instead
repeat(steps) {
onCodeInput(KeyCode.ARROW_RIGHT, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
}
return true
}
}
if (inputLogic.moveCursorByAndReturnIfInsideComposingWord(moveSteps)) {
// the shortcut below causes issues due to horrible handling of text fields by Firefox and forks
// issues:
// * setSelection "will cause the editor to call onUpdateSelection", see: https://developer.android.com/reference/android/view/inputmethod/InputConnection#setSelection(int,%20int)
// but Firefox is simply not doing this within the same word... WTF?
// https://github.com/Helium314/HeliBoard/issues/1139#issuecomment-2588169384
// * inputType is NOT if variant InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT (variant appears to always be 0)
// so we can't even only do it for browsers (identifying by app name will break for forks)
// best "solution" is not doing this for InputType variation 0 but this applies to the majority of text fields...
val variation = InputType.TYPE_MASK_VARIATION and Settings.getValues().mInputAttributes.mInputType
if (variation != 0 && inputLogic.moveCursorByAndReturnIfInsideComposingWord(moveSteps)) {
// no need to finish input and restart suggestions if we're still in the word
// this is a noticeable performance improvement
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps
inputLogic.mConnection.setSelection(newPosition, newPosition)
// this is a noticeable performance improvement when moving through long words
val newPosition = connection.expectedSelectionStart + moveSteps
connection.setSelection(newPosition, newPosition)
return true
}
inputLogic.finishInput()
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps
inputLogic.mConnection.setSelection(newPosition, newPosition)
val newPosition = connection.expectedSelectionStart + moveSteps
connection.setSelection(newPosition, newPosition)
inputLogic.restartSuggestionsOnWordTouchedByCursor(settings.current, keyboardSwitcher.currentKeyboardScript)
return true
}
private fun positiveMoveSteps(text: CharSequence, steps: Int): Int {
var actualSteps = 0
// corrected steps to avoid splitting chars belonging to the same codepoint
loopOverCodePoints(text) { cp, charCount ->
if (StringUtils.mightBeEmoji(cp)) return 0
actualSteps += charCount
actualSteps >= steps
}
return min(actualSteps, text.length)
}
private fun negativeMoveSteps(text: CharSequence, steps: Int): Int {
var actualSteps = 0
// corrected steps to avoid splitting chars belonging to the same codepoint
loopOverCodePointsBackwards(text) { cp, charCount ->
if (StringUtils.mightBeEmoji(cp)) return 0
actualSteps -= charCount
actualSteps <= steps
}
return -min(-actualSteps, text.length)
}
}

View file

@ -96,7 +96,7 @@ public final class KeyboardLayoutSet {
public static void onSystemLocaleChanged() {
clearKeyboardCache();
LocaleKeyboardInfosKt.clearCache();
SubtypeLocaleUtils.clearDisplayNameCache();
SubtypeLocaleUtils.clearSubtypeDisplayNameCache();
}
public static void onKeyboardThemeChanged() {

View file

@ -157,10 +157,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
} catch (KeyboardLayoutSetException e) {
Log.e(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
try {
final InputMethodSubtype qwerty = SubtypeUtilsAdditional.INSTANCE
.createEmojiCapableAdditionalSubtype(mRichImm.getCurrentSubtypeLocale(), SubtypeLocaleUtils.QWERTY, true);
final InputMethodSubtype defaults = SubtypeUtilsAdditional.INSTANCE.createDefaultSubtype(mRichImm.getCurrentSubtypeLocale());
mKeyboardLayoutSet = builder.setKeyboardGeometry(keyboardWidth, keyboardHeight)
.setSubtype(RichInputMethodSubtype.Companion.get(qwerty))
.setSubtype(RichInputMethodSubtype.Companion.get(defaults))
.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey)
.setNumberRowEnabled(settingsValues.mShowsNumberRow)
.setLanguageSwitchKeyEnabled(settingsValues.isLanguageSwitchKeyEnabled())
@ -169,9 +168,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
.setOneHandedModeEnabled(oneHandedModeEnabled)
.build();
mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState, oneHandedModeEnabled);
showToast("error loading the keyboard, falling back to qwerty", false);
showToast("error loading the keyboard, falling back to defaults", false);
} catch (KeyboardLayoutSetException e2) {
Log.e(TAG, "even fallback to qwerty failed: " + e2.mKeyboardId, e2.getCause());
Log.e(TAG, "even fallback to defaults failed: " + e2.mKeyboardId, e2.getCause());
}
}
}
@ -480,7 +479,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
if (mKeyboardViewWrapper.getOneHandedModeEnabled() == enabled) {
return;
}
mEmojiPalettesView.clearKeyboardCache();
final Settings settings = Settings.getInstance();
mKeyboardViewWrapper.setOneHandedModeEnabled(enabled);
mKeyboardViewWrapper.setOneHandedGravity(settings.getCurrent().mOneHandedModeGravity);
@ -515,9 +513,11 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
public void reloadKeyboard() {
if (mCurrentInputView != null)
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
if (mCurrentInputView == null)
return;
mEmojiPalettesView.clearKeyboardCache();
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
}
/**
@ -644,6 +644,12 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
}
public void trimMemory() {
if (mEmojiPalettesView != null) {
mEmojiPalettesView.clearKeyboardCache();
}
}
@SuppressLint("InflateParams")
public View onCreateInputView(@NonNull Context displayContext, final boolean isHardwareAcceleratedDrawingEnabled) {
if (mKeyboardView != null) {

View file

@ -405,14 +405,7 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
}
fun getUnusedThemeName(initialName: String, prefs: SharedPreferences): String {
val existingNames = prefs.all.keys.mapNotNull {
when {
it.startsWith(Settings.PREF_USER_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_COLORS_PREFIX)
it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_ALL_COLORS_PREFIX)
it.startsWith(Settings.PREF_USER_MORE_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_MORE_COLORS_PREFIX)
else -> null
}
}.toSortedSet()
val existingNames = getExistingThemeNames(prefs)
if (initialName !in existingNames) return initialName
var i = 1
while ("$initialName$i" in existingNames)
@ -420,11 +413,8 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
return "$initialName$i"
}
// returns false if not renamed due to invalid name or collision
fun renameUserColors(from: String, to: String, prefs: SharedPreferences): Boolean {
if (to.isBlank()) return false // don't want that
if (to == from) return true // nothing to do
val existingNames = prefs.all.keys.mapNotNull {
private fun getExistingThemeNames(prefs: SharedPreferences) =
prefs.all.keys.mapNotNull {
when {
it.startsWith(Settings.PREF_USER_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_COLORS_PREFIX)
it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) -> it.substringAfter(Settings.PREF_USER_ALL_COLORS_PREFIX)
@ -432,6 +422,12 @@ private constructor(val themeId: Int, @JvmField val mStyleId: Int) {
else -> null
}
}.toSortedSet()
// returns false if not renamed due to invalid name or collision
fun renameUserColors(from: String, to: String, prefs: SharedPreferences): Boolean {
if (to.isBlank()) return false // don't want that
if (to == from) return true // nothing to do
val existingNames = getExistingThemeNames(prefs)
if (to in existingNames) return false
// all good, now rename
prefs.edit {

View file

@ -27,6 +27,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import helium314.keyboard.keyboard.emoji.EmojiPageKeyboardView;
import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.keyboard.internal.KeyVisualAttributes;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
@ -34,7 +35,7 @@ import helium314.keyboard.latin.R;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.Colors;
import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.common.StringUtils;
import helium314.keyboard.latin.common.StringUtilsKt;
import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.suggestions.MoreSuggestions;
import helium314.keyboard.latin.suggestions.PopupSuggestionsView;
@ -147,6 +148,7 @@ public class KeyboardView extends View {
mPaint.setAntiAlias(true);
mTypeface = Settings.getInstance().getCustomTypeface();
setFitsSystemWindows(true);
}
@Nullable
@ -191,7 +193,8 @@ public class KeyboardView extends View {
invalidateAllKeys();
requestLayout();
mFontSizeMultiplier = mKeyboard.mId.isEmojiKeyboard()
? Settings.getValues().mFontSizeMultiplierEmoji
// In the case of EmojiKeyFit, the size of emojis is taken care of by the size of the keys
? (Settings.getValues().mEmojiKeyFit? 1 : Settings.getValues().mFontSizeMultiplierEmoji)
: Settings.getValues().mFontSizeMultiplier;
}
@ -423,10 +426,14 @@ public class KeyboardView extends View {
}
if (key.isEnabled()) {
if (StringUtils.mightBeEmoji(label))
if (StringUtilsKt.isEmoji(label))
paint.setColor(key.selectTextColor(params) | 0xFF000000); // ignore alpha for emojis (though actually color isn't applied anyway and we could just set white)
else if (key.hasActionKeyBackground())
paint.setColor(mColors.get(ColorType.ACTION_KEY_ICON));
else if (this instanceof EmojiPageKeyboardView)
paint.setColor(mColors.get(ColorType.EMOJI_KEY_TEXT));
else if (this instanceof PopupKeysKeyboardView)
paint.setColor(mColors.get(ColorType.POPUP_KEY_TEXT));
else
paint.setColor(key.selectTextColor(params));
// Set a drop shadow for the text if the shadow radius is positive value.
@ -610,7 +617,7 @@ public class KeyboardView extends View {
}
private void setKeyIconColor(Key key, Drawable icon, Keyboard keyboard) {
if (key.isAccentColored()) {
if (key.hasActionKeyBackground()) {
mColors.setColor(icon, ColorType.ACTION_KEY_ICON);
} else if (key.isShift() && keyboard != null) {
if (keyboard.mId.mElementId == KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED
@ -624,8 +631,7 @@ public class KeyboardView extends View {
} else if (key.getBackgroundType() != Key.BACKGROUND_TYPE_NORMAL) {
mColors.setColor(icon, ColorType.KEY_ICON);
} else if (this instanceof PopupKeysKeyboardView) {
// set color filter for long press comma key, should not trigger anywhere else
mColors.setColor(icon, ColorType.KEY_ICON);
mColors.setColor(icon, ColorType.POPUP_KEY_ICON);
} else if (key.getCode() == Constants.CODE_SPACE || key.getCode() == KeyCode.ZWNJ) {
// set color of default number pad space bar icon for Holo style, or for zero-width non-joiner (zwnj) on some layouts like nepal
mColors.setColor(icon, ColorType.KEY_ICON);

View file

@ -18,6 +18,7 @@ import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@ -25,7 +26,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.view.ContextThemeWrapper;
import helium314.keyboard.accessibility.AccessibilityUtils;
import helium314.keyboard.accessibility.MainKeyboardAccessibilityDelegate;
@ -57,6 +57,7 @@ import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.utils.KtxKt;
import helium314.keyboard.latin.utils.LanguageOnSpacebarUtils;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
import helium314.keyboard.latin.utils.TypefaceUtils;
import java.util.ArrayList;
@ -505,7 +506,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
mPopupKeysKeyboardCache.put(key, popupKeysKeyboard);
}
final View container = key.hasActionKeyBackground() ? mPopupKeysKeyboardForActionContainer
final View container = key.hasActionKeyPopups() ? mPopupKeysKeyboardForActionContainer
: mPopupKeysKeyboardContainer;
final PopupKeysKeyboardView popupKeysKeyboardView =
container.findViewById(R.id.popup_keys_keyboard_view);

View file

@ -328,12 +328,13 @@ public final class PopupKeysKeyboard extends Keyboard {
final PopupKeysKeyboardParams params = mParams;
final int popupKeyFlags = mParentKey.getPopupKeyLabelFlags();
final PopupKeySpec[] popupKeys = mParentKey.getPopupKeys();
final int background = mParentKey.hasActionKeyPopups() ? Key.BACKGROUND_TYPE_ACTION : Key.BACKGROUND_TYPE_NORMAL;
for (int n = 0; n < popupKeys.length; n++) {
final PopupKeySpec popupKeySpec = popupKeys[n];
final int row = n / params.mNumColumns;
final int x = params.getX(n, row);
final int y = params.getY(row);
final Key key = popupKeySpec.buildKey(x, y, popupKeyFlags, params);
final Key key = popupKeySpec.buildKey(x, y, popupKeyFlags, background, params);
params.markAsEdgeKey(key, row);
params.onAddKey(key);

View file

@ -71,6 +71,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
getEnabledClipboardToolbarKeys(context.prefs())
.forEach { toolbarKeys.add(createToolbarKey(context, KeyboardIconsSet.instance, it)) }
keyboardAttr.recycle()
fitsSystemWindows = true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

View file

@ -64,4 +64,4 @@ class ClipboardLayoutParams(ctx: Context) {
view.layoutParams = this
}
}
}
}

View file

@ -11,6 +11,7 @@ import static helium314.keyboard.keyboard.internal.keyboard_parser.EmojiParserKt
import android.content.SharedPreferences;
import android.text.TextUtils;
import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.settings.Defaults;
import helium314.keyboard.latin.utils.Log;
@ -34,8 +35,6 @@ import java.util.List;
*/
final class DynamicGridKeyboard extends Keyboard {
private static final String TAG = DynamicGridKeyboard.class.getSimpleName();
private static final int TEMPLATE_KEY_CODE_0 = 0x30;
private static final int TEMPLATE_KEY_CODE_1 = 0x31;
private final Object mLock = new Object();
private final SharedPreferences mPrefs;
@ -60,8 +59,8 @@ final class DynamicGridKeyboard extends Keyboard {
mBaseWidth = width - paddingWidth;
mOccupiedWidth = width;
final float spacerWidth = Settings.getValues().mSplitKeyboardSpacerRelativeWidth * mBaseWidth;
final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0);
final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1);
final Key key0 = getTemplateKey(Constants.RECENTS_TEMPLATE_KEY_CODE_0);
final Key key1 = getTemplateKey(Constants.RECENTS_TEMPLATE_KEY_CODE_1);
final int horizontalGap = Math.abs(key1.getX() - key0.getX()) - key0.getWidth();
final float widthScale = determineWidthScale(key0.getWidth() + horizontalGap);
mHorizontalGap = (int) (horizontalGap * widthScale);
@ -213,7 +212,7 @@ final class DynamicGridKeyboard extends Keyboard {
}
// fall back to creating the key
return new Key(getTemplateKey(TEMPLATE_KEY_CODE_0), null, null, Key.BACKGROUND_TYPE_EMPTY, code, null);
return new Key(getTemplateKey(Constants.RECENTS_TEMPLATE_KEY_CODE_0), null, null, Key.BACKGROUND_TYPE_EMPTY, code, null);
}
private Key getKeyByOutputText(final Collection<DynamicGridKeyboard> keyboards,
@ -227,7 +226,7 @@ final class DynamicGridKeyboard extends Keyboard {
}
// fall back to creating the key
return new Key(getTemplateKey(TEMPLATE_KEY_CODE_0), null, null, Key.BACKGROUND_TYPE_EMPTY, 0, outputText);
return new Key(getTemplateKey(Constants.RECENTS_TEMPLATE_KEY_CODE_0), null, null, Key.BACKGROUND_TYPE_EMPTY, 0, outputText);
}
public void loadRecentKeys(final Collection<DynamicGridKeyboard> keyboards) {

View file

@ -261,20 +261,6 @@ final class EmojiCategory {
return 0;
}
// Returns the view pager's page position for the categoryId
public int getPagerPageIdFromCategoryAndPageId(final int categoryId, final int categoryPageId) {
int sum = 0;
for (int i = 0; i < mShownCategories.size(); ++i) {
final CategoryProperties props = mShownCategories.get(i);
if (props.mCategoryId == categoryId) {
return sum + categoryPageId;
}
sum += props.getPageCount();
}
Log.w(TAG, "categoryId not found: " + categoryId);
return 0;
}
public int getRecentTabId() {
return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS);
}
@ -285,11 +271,11 @@ final class EmojiCategory {
}
// Returns a keyboard from the recycler view's adapter position.
public DynamicGridKeyboard getKeyboardFromAdapterPosition(final int position) {
if (position >= 0 && position < getCurrentCategoryPageCount()) {
return getKeyboard(mCurrentCategoryId, position);
public DynamicGridKeyboard getKeyboardFromAdapterPosition(int categoryId, final int position) {
if (position >= 0 && position < getCategoryPageCount(categoryId)) {
return getKeyboard(categoryId, position);
}
Log.w(TAG, "invalid position for categoryId : " + mCurrentCategoryId);
Log.w(TAG, "invalid position for categoryId : " + categoryId);
return null;
}

View file

@ -8,7 +8,7 @@ package helium314.keyboard.keyboard.emoji
import android.content.res.Resources
import android.view.View
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.latin.R
import helium314.keyboard.latin.settings.Settings
@ -47,7 +47,7 @@ internal class EmojiLayoutParams(res: Resources) {
emojiKeyboardHeight = emojiListHeight - emojiCategoryPageIdViewHeight - emojiListBottomMargin
}
fun setEmojiListProperties(vp: RecyclerView) {
fun setEmojiListProperties(vp: ViewPager2) {
val lp = vp.layoutParams as LinearLayout.LayoutParams
lp.height = emojiKeyboardHeight
lp.bottomMargin = emojiListBottomMargin

View file

@ -12,156 +12,32 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import helium314.keyboard.keyboard.Key;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardView;
import helium314.keyboard.latin.R;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import helium314.keyboard.latin.settings.Settings;
final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapter.ViewHolder>{
private static final String TAG = EmojiPalettesAdapter.class.getSimpleName();
private static final boolean DEBUG_PAGER = false;
private final int mCategoryId;
private final OnKeyEventListener mListener;
private final DynamicGridKeyboard mRecentsKeyboard;
private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews = new SparseArray<>();
private final EmojiCategory mEmojiCategory;
private int mActivePosition = 0;
public EmojiPalettesAdapter(final EmojiCategory emojiCategory,
final OnKeyEventListener listener) {
public EmojiPalettesAdapter(final EmojiCategory emojiCategory, int categoryId, final OnKeyEventListener listener) {
mEmojiCategory = emojiCategory;
mCategoryId = categoryId;
mListener = listener;
mRecentsKeyboard = mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0);
}
public void flushPendingRecentKeys() {
mRecentsKeyboard.flushPendingRecentKeys();
final KeyboardView recentKeyboardView =
mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
if (recentKeyboardView != null) {
recentKeyboardView.invalidateAllKeys();
}
}
public void addRecentKey(final Key key) {
if (Settings.getValues().mIncognitoModeEnabled) {
// We do not want to log recent keys while being in incognito
return;
}
if (mEmojiCategory.isInRecentTab()) {
mRecentsKeyboard.addPendingKey(key);
return;
}
mRecentsKeyboard.addKeyFirst(key);
final KeyboardView recentKeyboardView =
mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
if (recentKeyboardView != null) {
recentKeyboardView.invalidateAllKeys();
}
}
public void onPageScrolled() {
releaseCurrentKey(false /* withKeyRegistering */);
}
public void releaseCurrentKey(final boolean withKeyRegistering) {
// Make sure the delayed key-down event (highlight effect and haptic feedback) will be
// canceled.
final EmojiPageKeyboardView currentKeyboardView =
mActiveKeyboardViews.get(mActivePosition);
if (currentKeyboardView == null) {
return;
}
currentKeyboardView.releaseCurrentKey(withKeyRegistering);
}
/*
@Override
public Object instantiateItem(final ViewGroup container, final int position) {
if (DEBUG_PAGER) {
Log.d(TAG, "instantiate item: " + position);
}
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position);
if (oldKeyboardView != null) {
oldKeyboardView.deallocateMemory();
// This may be redundant but wanted to be safer..
mActiveKeyboardViews.remove(position);
}
final Keyboard keyboard =
mEmojiCategory.getKeyboardFromPagePosition(position);
final LayoutInflater inflater = LayoutInflater.from(container.getContext());
final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView) inflater.inflate(
R.layout.emoji_keyboard_page, container, false);
keyboardView.setKeyboard(keyboard);
keyboardView.setOnKeyEventListener(mListener);
container.addView(keyboardView);
mActiveKeyboardViews.put(position, keyboardView);
return keyboardView;
}
@Override
public void setPrimaryItem(final ViewGroup container, final int position,
final Object object) {
if (mActivePosition == position) {
return;
}
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition);
if (oldKeyboardView != null) {
oldKeyboardView.releaseCurrentKey(false);
oldKeyboardView.deallocateMemory();
}
mActivePosition = position;
}
@Override
public boolean isViewFromObject(final View view, final Object object) {
return view == object;
}
@Override
public void destroyItem(final ViewGroup container, final int position,
final Object object) {
if (DEBUG_PAGER) {
Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName());
}
final EmojiPageKeyboardView keyboardView = mActiveKeyboardViews.get(position);
if (keyboardView != null) {
keyboardView.deallocateMemory();
mActiveKeyboardViews.remove(position);
}
if (object instanceof View) {
container.removeView((View)object);
} else {
Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object);
}
}
*/
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
/*if (DEBUG_PAGER) {
Log.d(TAG, "instantiate item: " + viewType);
}
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(viewType);
if (oldKeyboardView != null) {
oldKeyboardView.deallocateMemory();
// This may be redundant but wanted to be safer..
mActiveKeyboardViews.remove(viewType);
}
final Keyboard keyboard =
mEmojiCategory.getKeyboardFromPagePosition(parent.getVerticalScrollbarPosition());*/
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView)inflater.inflate(
R.layout.emoji_keyboard_page, parent, false);
/*keyboardView.setKeyboard(keyboard);
keyboardView.setOnKeyEventListener(mListener);
parent.addView(keyboardView);
mActiveKeyboardViews.put(parent.getVerticalScrollbarPosition(), keyboardView);*/
return new ViewHolder(keyboardView);
}
@ -170,33 +46,22 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
if (DEBUG_PAGER) {
Log.d(TAG, "instantiate item: " + position);
}
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position);
if (oldKeyboardView != null) {
oldKeyboardView.deallocateMemory();
// This may be redundant but wanted to be safer..
mActiveKeyboardViews.remove(position);
}
final Keyboard keyboard =
mEmojiCategory.getKeyboardFromAdapterPosition(position);
mEmojiCategory.getKeyboardFromAdapterPosition(mCategoryId, position);
holder.getKeyboardView().setKeyboard(keyboard);
holder.getKeyboardView().setOnKeyEventListener(mListener);
//parent.addView(keyboardView);
mActiveKeyboardViews.put(position, holder.getKeyboardView());
}
/*if (mActivePosition == position) {
return;
}
final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition);
if (oldKeyboardView != null) {
oldKeyboardView.releaseCurrentKey(false);
oldKeyboardView.deallocateMemory();
}
mActivePosition = position;*/
@Override
public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
holder.getKeyboardView().releaseCurrentKey(false);
holder.getKeyboardView().deallocateMemory();
}
@Override
public int getItemCount() {
return mEmojiCategory.getCurrentCategoryPageCount();
return mEmojiCategory.getCategoryPageCount(mCategoryId);
}
static class ViewHolder extends RecyclerView.ViewHolder {
@ -210,7 +75,6 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
public EmojiPageKeyboardView getKeyboardView() {
return customView;
}
}
}

View file

@ -6,11 +6,15 @@
package helium314.keyboard.keyboard.emoji;
import java.util.HashMap;
import java.util.Map;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
@ -20,6 +24,7 @@ import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import helium314.keyboard.keyboard.Key;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardActionListener;
@ -41,8 +46,6 @@ import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.settings.SettingsValues;
import helium314.keyboard.latin.utils.ResourceUtils;
import org.jetbrains.annotations.NotNull;
import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
/**
@ -58,26 +61,135 @@ import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
*/
public final class EmojiPalettesView extends LinearLayout
implements View.OnClickListener, OnKeyEventListener {
private static final class PagerViewHolder extends RecyclerView.ViewHolder {
private long mCategoryId;
private PagerViewHolder(View itemView) {
super(itemView);
}
}
private final class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder> {
private boolean mInitialized;
private Map<Integer, RecyclerView> mViews = new HashMap<>(mEmojiCategory.getShownCategories().size());
private PagerAdapter(ViewPager2 pager) {
setHasStableIds(true);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
var categoryId = (int) getItemId(position);
setCurrentCategoryId(categoryId, false);
var recyclerView = mViews.get(position);
if (recyclerView != null) {
updateState(recyclerView, categoryId);
}
}
});
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
recyclerView.setItemViewCacheSize(mEmojiCategory.getShownCategories().size());
}
@NonNull
@Override
public PagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
var view = LayoutInflater.from(parent.getContext()).inflate(R.layout.emoji_category_view, parent, false);
var viewHolder = new PagerViewHolder(view);
var emojiRecyclerView = getRecyclerView(view);
emojiRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// Ignore this message. Only want the actual page selected.
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
updateState(recyclerView, viewHolder.mCategoryId);
}
});
emojiRecyclerView.setPersistentDrawingCache(PERSISTENT_NO_CACHE);
return viewHolder;
}
@Override
public void onBindViewHolder(PagerViewHolder holder, int position) {
holder.mCategoryId = getItemId(position);
var recyclerView = getRecyclerView(holder.itemView);
mViews.put(position, recyclerView);
recyclerView.setAdapter(new EmojiPalettesAdapter(mEmojiCategory, (int) holder.mCategoryId,
EmojiPalettesView.this));
if (! mInitialized) {
recyclerView.scrollToPosition(mEmojiCategory.getCurrentCategoryPageId());
mInitialized = true;
}
}
@Override
public int getItemCount() {
return mEmojiCategory.getShownCategories().size();
}
@Override
public void onViewDetachedFromWindow(PagerViewHolder holder) {
if (holder.mCategoryId == EmojiCategory.ID_RECENTS) {
// Needs to save pending updates for recent keys when we get out of the recents
// category because we don't want to move the recent emojis around while the user
// is in the recents category.
getRecentsKeyboard().flushPendingRecentKeys();
getRecyclerView(holder.itemView).getAdapter().notifyDataSetChanged();
}
}
@Override
public long getItemId(int position) {
return mEmojiCategory.getShownCategories().get(position).mCategoryId;
}
private static RecyclerView getRecyclerView(View view) {
return view.findViewById(R.id.emoji_keyboard_list);
}
private void updateState(@NonNull RecyclerView recyclerView, long categoryId) {
if (categoryId != mEmojiCategory.getCurrentCategoryId()) {
return;
}
final int offset = recyclerView.computeVerticalScrollOffset();
final int extent = recyclerView.computeVerticalScrollExtent();
final int range = recyclerView.computeVerticalScrollRange();
final float percentage = offset / (float) (range - extent);
final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageCount();
final int a = (int) (percentage * currentCategorySize);
final float b = percentage * currentCategorySize - a;
mEmojiCategoryPageIndicatorView.setCategoryPageId(currentCategorySize, a, b);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
final int firstCompleteVisibleBoard = layoutManager.findFirstCompletelyVisibleItemPosition();
final int firstVisibleBoard = layoutManager.findFirstVisibleItemPosition();
mEmojiCategory.setCurrentCategoryPageId(
firstCompleteVisibleBoard > 0 ? firstCompleteVisibleBoard : firstVisibleBoard);
}
}
private boolean initialized = false;
// keep the indicator in case emoji view is changed to tabs / viewpager
private final boolean mCategoryIndicatorEnabled;
private final int mCategoryIndicatorDrawableResId;
private final int mCategoryIndicatorBackgroundResId;
private final int mCategoryPageIndicatorColor;
private final Colors mColors;
private EmojiPalettesAdapter mEmojiPalettesAdapter;
private final EmojiLayoutParams mEmojiLayoutParams;
private final LinearLayoutManager mEmojiLayoutManager;
private LinearLayout mTabStrip;
private RecyclerView mEmojiRecyclerView;
private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
private final EmojiCategory mEmojiCategory;
private ImageView mCurrentTab = null;
private ViewPager2 mPager;
public EmojiPalettesView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.emojiPalettesViewStyle);
@ -96,16 +208,8 @@ public final class EmojiPalettesView extends LinearLayout
final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView);
mEmojiCategory = new EmojiCategory(context, layoutSet, emojiPalettesViewAttr);
mCategoryIndicatorEnabled = emojiPalettesViewAttr.getBoolean(
R.styleable.EmojiPalettesView_categoryIndicatorEnabled, false);
mCategoryIndicatorDrawableResId = emojiPalettesViewAttr.getResourceId(
R.styleable.EmojiPalettesView_categoryIndicatorDrawable, 0);
mCategoryIndicatorBackgroundResId = emojiPalettesViewAttr.getResourceId(
R.styleable.EmojiPalettesView_categoryIndicatorBackground, 0);
mCategoryPageIndicatorColor = emojiPalettesViewAttr.getColor( // todo: remove this and related attr
R.styleable.EmojiPalettesView_categoryPageIndicatorColor, 0);
emojiPalettesViewAttr.recycle();
mEmojiLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
setFitsSystemWindows(true);
}
@Override
@ -121,13 +225,6 @@ public final class EmojiPalettesView extends LinearLayout
setMeasuredDimension(width, height);
}
// todo (maybe): bring back the holo indicator thing?
// just some 2 dp high strip
// would probably need a vertical linear layout
// better not, would complicate stuff again
// when decided to definitely not bring it back:
// remove mCategoryIndicatorEnabled, mCategoryIndicatorDrawableResId, mCategoryIndicatorBackgroundResId
// and the attrs categoryIndicatorDrawable, categoryIndicatorEnabled, categoryIndicatorBackground (and the connected drawables)
private void addTab(final LinearLayout host, final int categoryId) {
final ImageView iconView = new ImageView(getContext());
mColors.setBackground(iconView, ColorType.STRIP_BACKGROUND);
@ -149,59 +246,14 @@ public final class EmojiPalettesView extends LinearLayout
for (final EmojiCategory.CategoryProperties properties : mEmojiCategory.getShownCategories()) {
addTab(mTabStrip, properties.mCategoryId);
}
// mTabStrip.setOnTabChangedListener(this); // now onClickListener
/* final TabWidget tabWidget = mTabStrip.getTabWidget();
tabWidget.setStripEnabled(mCategoryIndicatorEnabled);
if (mCategoryIndicatorEnabled) {
// On TabWidget's strip, what looks like an indicator is actually a background.
// And what looks like a background are actually left and right drawables.
tabWidget.setBackgroundResource(mCategoryIndicatorDrawableResId);
tabWidget.setLeftStripDrawable(mCategoryIndicatorBackgroundResId);
tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId);
tabWidget.setBackgroundColor(mColors.get(ColorType.EMOJI_CATEGORY_SELECTED));
}
*/
mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this);
mEmojiRecyclerView = findViewById(R.id.emoji_keyboard_list);
mEmojiRecyclerView.setLayoutManager(mEmojiLayoutManager);
mEmojiRecyclerView.setAdapter(mEmojiPalettesAdapter);
mEmojiRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull @NotNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// Ignore this message. Only want the actual page selected.
}
@Override
public void onScrolled(@NonNull @NotNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mEmojiPalettesAdapter.onPageScrolled();
final int offset = recyclerView.computeVerticalScrollOffset();
final int extent = recyclerView.computeVerticalScrollExtent();
final int range = recyclerView.computeVerticalScrollRange();
final float percentage = offset / (float) (range - extent);
final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageCount();
final int a = (int) (percentage * currentCategorySize);
final float b = percentage * currentCategorySize - a;
mEmojiCategoryPageIndicatorView.setCategoryPageId(currentCategorySize, a, b);
final int firstCompleteVisibleBoard = mEmojiLayoutManager.findFirstCompletelyVisibleItemPosition();
final int firstVisibleBoard = mEmojiLayoutManager.findFirstVisibleItemPosition();
mEmojiCategory.setCurrentCategoryPageId(
firstCompleteVisibleBoard > 0 ? firstCompleteVisibleBoard : firstVisibleBoard);
}
});
mEmojiRecyclerView.setPersistentDrawingCache(PERSISTENT_NO_CACHE);
mEmojiLayoutParams.setEmojiListProperties(mEmojiRecyclerView);
mPager = findViewById(R.id.emoji_pager);
mPager.setAdapter(new PagerAdapter(mPager));
mEmojiLayoutParams.setEmojiListProperties(mPager);
mEmojiCategoryPageIndicatorView = findViewById(R.id.emoji_category_page_id_view);
mEmojiLayoutParams.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView);
setCurrentCategoryAndPageId(mEmojiCategory.getCurrentCategoryId(), mEmojiCategory.getCurrentCategoryPageId(), true);
setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true);
mEmojiCategoryPageIndicatorView.setColors(mColors.get(ColorType.EMOJI_CATEGORY_SELECTED), mColors.get(ColorType.STRIP_BACKGROUND));
initialized = true;
@ -219,7 +271,7 @@ public final class EmojiPalettesView extends LinearLayout
AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(KeyCode.NOT_SPECIFIED, this);
final int categoryId = ((Long) tag).intValue();
if (categoryId != mEmojiCategory.getCurrentCategoryId()) {
setCurrentCategoryAndPageId(categoryId, 0, false);
setCurrentCategoryId(categoryId, false);
updateEmojiCategoryPageIdView();
}
}
@ -244,7 +296,7 @@ public final class EmojiPalettesView extends LinearLayout
*/
@Override
public void onReleaseKey(final Key key) {
mEmojiPalettesAdapter.addRecentKey(key);
addRecentKey(key);
final int code = key.getCode();
if (code == KeyCode.MULTIPLE_CODE_POINTS) {
mKeyboardActionListener.onTextInput(key.getOutputText());
@ -269,13 +321,22 @@ public final class EmojiPalettesView extends LinearLayout
setupBottomRowKeyboard(editorInfo, keyboardActionListener);
final KeyDrawParams params = new KeyDrawParams();
params.updateParams(mEmojiLayoutParams.getBottomRowKeyboardHeight(), keyVisualAttr);
if (mEmojiRecyclerView.getAdapter() == null) {
mEmojiRecyclerView.setAdapter(mEmojiPalettesAdapter);
setCurrentCategoryAndPageId(mEmojiCategory.getCurrentCategoryId(), mEmojiCategory.getCurrentCategoryPageId(), true);
}
setupSidePadding();
}
private void addRecentKey(final Key key) {
if (Settings.getValues().mIncognitoModeEnabled) {
// We do not want to log recent keys while being in incognito
return;
}
if (mEmojiCategory.isInRecentTab()) {
getRecentsKeyboard().addPendingKey(key);
return;
}
getRecentsKeyboard().addKeyFirst(key);
mPager.getAdapter().notifyItemChanged(mEmojiCategory.getRecentTabId());
}
private void setupBottomRowKeyboard(final EditorInfo editorInfo, final KeyboardActionListener keyboardActionListener) {
MainKeyboardView keyboardView = findViewById(R.id.bottom_row_keyboard);
keyboardView.setKeyboardActionListener(keyboardActionListener);
@ -295,11 +356,11 @@ public final class EmojiPalettesView extends LinearLayout
final float rightPadding = keyboardAttr.getFraction(R.styleable.Keyboard_keyboardRightPadding,
keyboardWidth, keyboardWidth, 0f) * sv.mSidePaddingScale;
keyboardAttr.recycle();
mEmojiRecyclerView.setPadding(
mPager.setPadding(
(int) leftPadding,
mEmojiRecyclerView.getPaddingTop(),
mPager.getPaddingTop(),
(int) rightPadding,
mEmojiRecyclerView.getPaddingBottom()
mPager.getPaddingBottom()
);
mEmojiCategoryPageIndicatorView.setPadding(
(int) leftPadding,
@ -312,9 +373,11 @@ public final class EmojiPalettesView extends LinearLayout
public void stopEmojiPalettes() {
if (!initialized) return;
mEmojiPalettesAdapter.releaseCurrentKey(true);
mEmojiPalettesAdapter.flushPendingRecentKeys();
mEmojiRecyclerView.setAdapter(null);
getRecentsKeyboard().flushPendingRecentKeys();
}
private DynamicGridKeyboard getRecentsKeyboard() {
return mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0);
}
public void setKeyboardActionListener(final KeyboardActionListener listener) {
@ -330,34 +393,33 @@ public final class EmojiPalettesView extends LinearLayout
mEmojiCategory.getCurrentCategoryPageId(), 0.0f);
}
private void setCurrentCategoryAndPageId(final int categoryId, final int categoryPageId, final boolean force) {
private void setCurrentCategoryId(final int categoryId, final boolean initial) {
final int oldCategoryId = mEmojiCategory.getCurrentCategoryId();
final int oldCategoryPageId = mEmojiCategory.getCurrentCategoryPageId();
if (oldCategoryId == EmojiCategory.ID_RECENTS && categoryId != EmojiCategory.ID_RECENTS) {
// Needs to save pending updates for recent keys when we get out of the recents
// category because we don't want to move the recent emojis around while the user
// is in the recents category.
mEmojiPalettesAdapter.flushPendingRecentKeys();
}
if (force || oldCategoryId != categoryId || oldCategoryPageId != categoryPageId) {
if (initial || oldCategoryId != categoryId) {
mEmojiCategory.setCurrentCategoryId(categoryId);
mEmojiCategory.setCurrentCategoryPageId(categoryPageId);
mEmojiPalettesAdapter.notifyDataSetChanged();
mEmojiRecyclerView.scrollToPosition(categoryPageId);
if (mPager.getScrollState() != ViewPager2.SCROLL_STATE_DRAGGING) {
// Not swiping
mPager.setCurrentItem(mEmojiCategory.getTabIdFromCategoryId(
mEmojiCategory.getCurrentCategoryId()), ! initial);
}
final View old = mTabStrip.findViewWithTag((long) oldCategoryId);
final View current = mTabStrip.findViewWithTag((long) categoryId);
if (old instanceof ImageView)
Settings.getValues().mColors.setColor((ImageView) old, ColorType.EMOJI_CATEGORY);
if (current instanceof ImageView)
Settings.getValues().mColors.setColor((ImageView) current, ColorType.EMOJI_CATEGORY_SELECTED);
}
final View old = mTabStrip.findViewWithTag((long) oldCategoryId);
final View current = mTabStrip.findViewWithTag((long) categoryId);
if (old instanceof ImageView)
Settings.getValues().mColors.setColor((ImageView) old, ColorType.EMOJI_CATEGORY);
if (current instanceof ImageView)
Settings.getValues().mColors.setColor((ImageView) current, ColorType.EMOJI_CATEGORY_SELECTED);
}
public void clearKeyboardCache() {
if (!initialized) {
return;
}
mEmojiCategory.clearKeyboardCache();
mPager.getAdapter().notifyDataSetChanged();
}
}
}

View file

@ -0,0 +1,24 @@
package helium314.keyboard.keyboard.emoji
import android.content.Context
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.prefs
object SupportedEmojis {
private val unsupportedEmojis = hashSetOf<String>()
fun load(context: Context) {
val maxSdk = context.prefs().getInt(Settings.PREF_EMOJI_MAX_SDK, Defaults.PREF_EMOJI_MAX_SDK)
unsupportedEmojis.clear()
context.assets.open("emoji/minApi.txt").reader().readLines().forEach {
val s = it.split(" ")
val minApi = s.first().toInt()
if (minApi > maxSdk)
unsupportedEmojis.addAll(s.drop(1))
}
}
fun isUnsupported(emoji: String) = emoji in unsupportedEmojis
}

View file

@ -108,7 +108,7 @@ public final class KeyPreviewChoreographer {
final boolean hasPopupKeys = (key.getPopupKeys() != null);
keyPreviewView.setPreviewBackground(hasPopupKeys, keyPreviewPosition);
final Colors colors = Settings.getValues().mColors;
colors.setBackground(keyPreviewView, ColorType.KEY_PREVIEW);
colors.setBackground(keyPreviewView, ColorType.KEY_PREVIEW_BACKGROUND);
// The key preview is placed vertically above the top edge of the parent key with an
// arbitrary offset.

View file

@ -15,8 +15,7 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import androidx.appcompat.widget.AppCompatTextView;
import android.widget.TextView;
import helium314.keyboard.keyboard.Key;
import helium314.keyboard.latin.R;
@ -25,10 +24,9 @@ import helium314.keyboard.latin.settings.Settings;
import java.util.HashSet;
/**
* The pop up key preview view.
*/
public class KeyPreviewView extends AppCompatTextView {
/** The pop up key preview view. */
// Android Studio complains about TextView, but we're not using tint or auto-size that should be the relevant differences
public class KeyPreviewView extends TextView {
public static final int POSITION_MIDDLE = 0;
public static final int POSITION_LEFT = 1;
public static final int POSITION_RIGHT = 2;

View file

@ -129,7 +129,7 @@ public final class KeyVisualAttributes {
// when? -> hasShiftedLetterHint and isShiftedLetterActivated -> both are label flags
mShiftedLetterHintActivatedColor = keyAttr.getColor(
R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
mPreviewTextColor = colors.get(ColorType.KEY_TEXT);
mPreviewTextColor = colors.get(ColorType.KEY_PREVIEW_TEXT);
mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr,
R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, 0.0f);

View file

@ -48,7 +48,7 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
if (id.isEmojiKeyboard) {
mParams.mAllowRedundantPopupKeys = true
readAttributes(R.xml.kbd_emoji)
keysInRows = EmojiParser(mParams, mContext, Settings.getValues().mEmojiMaxSdk).parse()
keysInRows = EmojiParser(mParams, mContext).parse()
} else {
try {
setupParams()
@ -176,7 +176,7 @@ open class KeyboardBuilder<KP : KeyboardParams>(protected val mContext: Context,
val relativeWidthSumNew = row.sumOf { it.mWidth }
val widthFactor = relativeWidthSum / relativeWidthSumNew
// re-calculate absolute sizes and positions
var currentX = 0f
var currentX = mParams.mLeftPadding.toFloat()
row.forEach {
it.mWidth *= widthFactor
it.setAbsoluteDimensions(currentX, y)

View file

@ -80,6 +80,7 @@ public final class KeyboardState {
private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
private static final int SWITCH_STATE_SYMBOL = 2;
private static final int SWITCH_STATE_NUMPAD = 3;
private static final int SWITCH_STATE_NUMPAD_BEGIN = 9;
private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 4;
private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 5;
private static final int SWITCH_STATE_MOMENTARY_ALPHA_SHIFT = 6;
@ -403,7 +404,7 @@ public final class KeyboardState {
mMode = MODE_NUMPAD;
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
mSwitchActions.setNumpadKeyboard();
mSwitchState = withSliding ? SWITCH_STATE_MOMENTARY_TO_NUMPAD : SWITCH_STATE_NUMPAD;
mSwitchState = withSliding ? SWITCH_STATE_MOMENTARY_TO_NUMPAD : SWITCH_STATE_NUMPAD_BEGIN;
}
public void toggleNumpad(final boolean withSliding, final int autoCapsFlags, final int recapitalizeMode,
@ -789,6 +790,17 @@ public final class KeyboardState {
mPrevSymbolsKeyboardWasShifted = false;
}
break;
case SWITCH_STATE_NUMPAD:
// Switch back to alpha keyboard mode if user types one or more non-space/enter
// characters followed by a space/enter.
if (isSpaceOrEnter(code) && Settings.getValues().mAlphaAfterNumpadAndSpace) {
toggleNumpad(false, autoCapsFlags, recapitalizeMode, true, false);
}
break;
case SWITCH_STATE_NUMPAD_BEGIN:
if (!isSpaceOrEnter(code))
mSwitchState = SWITCH_STATE_NUMPAD;
break;
}
// If the code is a letter, update keyboard shift state.
@ -833,6 +845,7 @@ public final class KeyboardState {
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE -> "MOMENTARY-SYMBOL-MORE";
case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT -> "MOMENTARY-ALPHA_SHIFT";
case SWITCH_STATE_NUMPAD -> "NUMPAD";
case SWITCH_STATE_NUMPAD_BEGIN -> "NUMPAD-BEGIN";
case SWITCH_STATE_MOMENTARY_TO_NUMPAD -> "MOMENTARY-TO-NUMPAD";
case SWITCH_STATE_MOMENTARY_FROM_NUMPAD -> "MOMENTARY-FROM-NUMPAD";
default -> null;

View file

@ -68,11 +68,9 @@ public final class PopupKeySpec {
}
@NonNull
public Key buildKey(final int x, final int y, final int labelFlags,
@NonNull final KeyboardParams params) {
return new Key(mLabel, mIconName, mCode, mOutputText, null /* hintLabel */, labelFlags,
Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultAbsoluteKeyWidth, params.mDefaultAbsoluteRowHeight,
params.mHorizontalGap, params.mVerticalGap);
public Key buildKey(final int x, final int y, final int labelFlags, final int background, @NonNull final KeyboardParams params) {
return new Key(mLabel, mIconName, mCode, mOutputText, null, labelFlags, background, x, y,
params.mDefaultAbsoluteKeyWidth, params.mDefaultAbsoluteRowHeight, params.mHorizontalGap, params.mVerticalGap);
}
@Override

View file

@ -5,38 +5,63 @@ import android.content.Context
import helium314.keyboard.keyboard.Key
import helium314.keyboard.keyboard.Key.KeyParams
import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.emoji.SupportedEmojis
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.ResourceUtils
import helium314.keyboard.latin.utils.prefs
import java.util.Collections
import kotlin.math.sqrt
class EmojiParser(private val params: KeyboardParams, private val context: Context, private val maxSdk: Int) {
class EmojiParser(private val params: KeyboardParams, private val context: Context) {
fun parse(): ArrayList<ArrayList<KeyParams>> {
val emojiArrayId = when (params.mId.mElementId) {
KeyboardId.ELEMENT_EMOJI_RECENTS -> R.array.emoji_recents
KeyboardId.ELEMENT_EMOJI_CATEGORY1 -> R.array.emoji_smileys_emotion
KeyboardId.ELEMENT_EMOJI_CATEGORY2 -> R.array.emoji_people_body
KeyboardId.ELEMENT_EMOJI_CATEGORY3 -> R.array.emoji_animals_nature
KeyboardId.ELEMENT_EMOJI_CATEGORY4 -> R.array.emoji_food_drink
KeyboardId.ELEMENT_EMOJI_CATEGORY5 -> R.array.emoji_travel_places
KeyboardId.ELEMENT_EMOJI_CATEGORY6 -> R.array.emoji_activities
KeyboardId.ELEMENT_EMOJI_CATEGORY7 -> R.array.emoji_objects
KeyboardId.ELEMENT_EMOJI_CATEGORY8 -> R.array.emoji_symbols
KeyboardId.ELEMENT_EMOJI_CATEGORY9 -> R.array.emoji_flags
KeyboardId.ELEMENT_EMOJI_CATEGORY10 -> R.array.emoji_emoticons
else -> throw(IllegalStateException("can only parse emoji categories where an array exists"))
val emojiFileName = when (params.mId.mElementId) {
KeyboardId.ELEMENT_EMOJI_CATEGORY1 -> "SMILEYS_AND_EMOTION.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY2 -> "PEOPLE_AND_BODY.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY3 -> "ANIMALS_AND_NATURE.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY4 -> "FOOD_AND_DRINK.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY5 -> "TRAVEL_AND_PLACES.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY6 -> "ACTIVITIES.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY7 -> "OBJECTS.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY8 -> "SYMBOLS.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY9 -> "FLAGS.txt"
KeyboardId.ELEMENT_EMOJI_CATEGORY10 -> "EMOTICONS.txt"
else -> null
}
val emojiArray = context.resources.getStringArray(emojiArrayId)
val popupEmojisArray = if (params.mId.mElementId != KeyboardId.ELEMENT_EMOJI_CATEGORY2) null
else context.resources.getStringArray(R.array.emoji_people_body_more)
if (popupEmojisArray != null && emojiArray.size != popupEmojisArray.size)
throw(IllegalStateException("Inconsistent array size between codesArray and popupKeysArray"))
val emojiLines = if (emojiFileName == null) {
listOf( // special template keys for recents category
StringUtils.newSingleCodePointString(Constants.RECENTS_TEMPLATE_KEY_CODE_0),
StringUtils.newSingleCodePointString(Constants.RECENTS_TEMPLATE_KEY_CODE_1),
)
} else {
context.assets.open("emoji/$emojiFileName").reader().use { it.readLines() }
}
val defaultSkinTone = context.prefs().getString(Settings.PREF_EMOJI_SKIN_TONE, Defaults.PREF_EMOJI_SKIN_TONE)!!
if (params.mId.mElementId == KeyboardId.ELEMENT_EMOJI_CATEGORY2 && defaultSkinTone != "") {
// adjust PEOPLE_AND_BODY if we have a non-yellow default skin tone
val modifiedLines = emojiLines.map {
val split = it.splitOnWhitespace().toMutableList()
// find the line containing the skin tone, and swap with first
val foundIndex = split.indexOfFirst { it.contains(defaultSkinTone) }
if (foundIndex > 0) {
Collections.swap(split, 0, foundIndex)
}
split.joinToString(" ")
}
return parseLines(modifiedLines)
}
return parseLines(emojiLines)
}
val row = ArrayList<KeyParams>(emojiArray.size)
private fun parseLines(lines: List<String>): ArrayList<ArrayList<KeyParams>> {
val row = ArrayList<KeyParams>(lines.size)
var currentX = params.mLeftPadding.toFloat()
val currentY = params.mTopPadding.toFloat() // no need to ever change, assignment to rows into rows is done in DynamicGridKeyboard
@ -44,14 +69,20 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
// this is a bit long, but ensures that emoji size stays the same, independent of these settings
// we also ignore side padding for key width, and prefer fewer keys per row over narrower keys
val defaultKeyWidth = ResourceUtils.getDefaultKeyboardWidth(context) * params.mDefaultKeyWidth
val keyWidth = defaultKeyWidth * sqrt(Settings.getValues().mKeyboardHeightScale)
var keyWidth = defaultKeyWidth * sqrt(Settings.getValues().mKeyboardHeightScale)
val defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false)
val defaultBottomPadding = context.resources.getFraction(R.fraction.config_keyboard_bottom_padding_holo, defaultKeyboardHeight, defaultKeyboardHeight)
val emojiKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(context.resources, false) * 0.75f + params.mVerticalGap - defaultBottomPadding - context.resources.getDimensionPixelSize(R.dimen.config_emoji_category_page_id_height)
val keyHeight = emojiKeyboardHeight * params.mDefaultRowHeight * Settings.getValues().mKeyboardHeightScale // still apply height scale to key
var keyHeight = emojiKeyboardHeight * params.mDefaultRowHeight * Settings.getValues().mKeyboardHeightScale // still apply height scale to key
emojiArray.forEachIndexed { i, codeArraySpec ->
val keyParams = parseEmojiKey(codeArraySpec, popupEmojisArray?.get(i)?.takeIf { it.isNotEmpty() }) ?: return@forEachIndexed
if (Settings.getValues().mEmojiKeyFit) {
keyWidth *= Settings.getValues().mFontSizeMultiplierEmoji
keyHeight *= Settings.getValues().mFontSizeMultiplierEmoji
}
lines.forEach { line ->
val keyParams = parseEmojiKeyNew(line) ?: return@forEach
keyParams.xPos = currentX
keyParams.yPos = currentY
keyParams.mAbsoluteWidth = keyWidth
@ -62,44 +93,30 @@ class EmojiParser(private val params: KeyboardParams, private val context: Conte
return arrayListOf(row)
}
private fun getLabelAndCode(spec: String): Pair<String, Int>? {
val specAndSdk = spec.split("||")
if (specAndSdk.getOrNull(1)?.toIntOrNull()?.let { it > maxSdk } == true) return null
if ("," !in specAndSdk.first()) {
val code = specAndSdk.first().toIntOrNull(16) ?: return specAndSdk.first() to KeyCode.MULTIPLE_CODE_POINTS // text emojis
val label = StringUtils.newSingleCodePointString(code)
return label to code
private fun parseEmojiKeyNew(line: String): KeyParams? {
if (!line.contains(" ") || params.mId.mElementId == KeyboardId.ELEMENT_EMOJI_CATEGORY10) {
// single emoji without popups, or emoticons (there is one that contains space...)
return if (SupportedEmojis.isUnsupported(line)) null
else KeyParams(line, line.getCode(), null, null, Key.LABEL_FLAGS_FONT_NORMAL, params)
}
val labelBuilder = StringBuilder()
for (codePointString in specAndSdk.first().split(",")) {
val cp = codePointString.toInt(16)
labelBuilder.appendCodePoint(cp)
}
return labelBuilder.toString() to KeyCode.MULTIPLE_CODE_POINTS
}
private fun parseEmojiKey(spec: String, popupKeysString: String? = null): KeyParams? {
val (label, code) = getLabelAndCode(spec) ?: return null
val sb = StringBuilder()
popupKeysString?.split(";")?.let { popupKeys ->
popupKeys.forEach {
val (mkLabel, _) = getLabelAndCode(it) ?: return@forEach
sb.append(mkLabel).append(",")
}
}
val popupKeysSpec = if (sb.isNotEmpty()) {
sb.deleteCharAt(sb.length - 1)
sb.toString()
} else null
val split = line.split(" ")
val label = split.first()
if (SupportedEmojis.isUnsupported(label)) return null
val popupKeysSpec = split.drop(1).filterNot { SupportedEmojis.isUnsupported(it) }
.takeIf { it.isNotEmpty() }?.joinToString(",")
return KeyParams(
label,
code,
label.getCode(),
if (popupKeysSpec != null) EMOJI_HINT_LABEL else null,
popupKeysSpec,
Key.LABEL_FLAGS_FONT_NORMAL,
params
)
}
private fun String.getCode(): Int =
if (StringUtils.codePointCount(this) != 1) KeyCode.MULTIPLE_CODE_POINTS
else Character.codePointAt(this, 0)
}
const val EMOJI_HINT_LABEL = ""
const val EMOJI_HINT_LABEL = ""

View file

@ -237,7 +237,7 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co
{ it.label == KeyLabel.PERIOD || it.groupId == KeyData.GROUP_PERIOD},
{ baseKeys.last()[1].copy(newGroupId = 2, newType = baseKeys.last()[1].type ?: it.type) }
)
baseKeys.removeLast()
baseKeys.removeAt(baseKeys.lastIndex)
}
// add zwnj key next to space if necessary
val spaceIndex = functionalKeysBottom.indexOfFirst { it.label == KeyLabel.SPACE && it.width <= 0 } // width could be 0 or -1

View file

@ -59,8 +59,7 @@ object LayoutParser {
/** Parse simple layouts, defined only as rows of (normal) keys with popup keys. */
fun parseSimpleString(layoutText: String): List<List<KeyData>> {
val rowStrings = layoutText.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex()).filter { it.isNotBlank() }
return rowStrings.map { row ->
return LayoutUtils.getSimpleRowStrings(layoutText).map { row ->
row.split("\n").mapNotNull { parseKey(it) }
}
}

View file

@ -11,6 +11,7 @@ import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.splitOnFirstSpacesOnly
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.SpacedTokens
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import java.io.InputStream
import java.util.Locale
@ -44,7 +45,7 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
"mns" -> Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO
else -> 0
}
val tlds = getLocaleTlds(locale) // todo: USE IT
val tlds = getLocaleTlds(locale)
init {
readStream(dataStream, false, true)
@ -83,18 +84,12 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
READER_MODE_EXTRA_KEYS -> if (!onlyPopupKeys) addExtraKey(line.split(colonSpaceRegex, 2))
READER_MODE_LABELS -> if (!onlyPopupKeys) addLabel(line.split(colonSpaceRegex, 2))
READER_MODE_NUMBER_ROW -> localizedNumberKeys = line.splitOnWhitespace()
READER_MODE_TLD -> line.splitOnWhitespace().forEach { tlds.add(".$it") }
READER_MODE_TLD -> SpacedTokens(line).forEach { tlds.add(".$it") }
}
}
}
}
fun addDefaultTlds(locale: Locale) {
if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country))
tlds.add(".eu")
tlds.addAll(defaultTlds.splitOnWhitespace())
}
/** Pair(extraKeysLeft, extraKeysRight) */
fun getTabletExtraKeys(elementId: Int): Pair<List<KeyData>, List<KeyData>> {
val flags = Key.LABEL_FLAGS_FONT_DEFAULT
@ -205,7 +200,6 @@ private fun createLocaleKeyTexts(context: Context, params: KeyboardParams, popup
if (locale == params.mId.locale) return@forEach
lkt.addFile(getStreamForLocale(locale, context), true)
}
lkt.addDefaultTlds(params.mId.locale)
when (popupKeysSetting) {
POPUP_KEYS_MAIN -> lkt.addFile(context.assets.open("$LOCALE_TEXTS_FOLDER/more_popups_main.txt"), false)
POPUP_KEYS_MORE -> lkt.addFile(context.assets.open("$LOCALE_TEXTS_FOLDER/more_popups_more.txt"), false)
@ -227,19 +221,27 @@ private fun getStreamForLocale(locale: Locale, context: Context) =
}
private fun getLocaleTlds(locale: Locale): LinkedHashSet<String> {
val tlds = getDefaultTlds(locale)
val ccLower = locale.country.lowercase()
val tlds = LinkedHashSet<String>()
if (ccLower.isEmpty() || locale.language == SubtypeLocaleUtils.NO_LANGUAGE)
return tlds
specialCountryTlds.forEach {
if (ccLower != it.first) return@forEach
tlds.addAll(it.second.splitOnWhitespace())
return tlds
tlds.addAll(SpacedTokens(it.second))
return@getLocaleTlds tlds
}
tlds.add(".$ccLower")
return tlds
}
private fun getDefaultTlds(locale: Locale): LinkedHashSet<String> {
val tlds = linkedSetOf<String>()
tlds.addAll(SpacedTokens(defaultTlds))
if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country))
tlds.add(".eu")
return tlds
}
fun clearCache() = localeKeyboardInfosCache.clear()
// cache the texts, so they don't need to be read over and over
@ -263,9 +265,9 @@ private fun getCurrencyKey(locale: Locale): Pair<String, List<String>> {
return euro
if (locale.toString().matches(euroLocales))
return euro
if (locale.language.matches("ca|eu|lb|mt".toRegex()))
if (locale.language.matches("ca|eu|lb|mt|pms".toRegex()))
return euro
if (locale.language.matches("fa|iw|ko|lo|mn|ne|si|th|uk|vi|km".toRegex()))
if (locale.language.matches("ak|dag|ee|fa|gaa|ha|he|ig|iw|lo|ko|km|mn|ne|si|th|uk|vi|yo".toRegex()))
return genericCurrencyKey(getCurrency(locale))
if (locale.language == "hy")
return dram
@ -291,17 +293,24 @@ private fun getCurrency(locale: Locale): String {
if (locale.country == "BD") return ""
if (locale.country == "LK") return "රු"
return when (locale.language) {
"ak" -> "¢"
"dag" -> "¢"
"ee" -> "¢"
"fa" -> ""
"iw" -> ""
"ko" -> ""
"gaa" -> "¢"
"ha" -> ""
"ig" -> ""
"iw", "he" -> ""
"lo" -> ""
"km" -> ""
"ko" -> ""
"mn" -> ""
"ne" -> "रु."
"si" -> "රු"
"th" -> "฿"
"uk" -> ""
"vi" -> ""
"km" -> ""
"yo" -> ""
else -> "$"
}
}

View file

@ -24,15 +24,15 @@ object KeyCode {
const val UNSPECIFIED = 0
const val CTRL = -1
const val CTRL_LOCK = -2
//const val CTRL_LOCK = -2
const val ALT = -3
const val ALT_LOCK = -4
//const val ALT_LOCK = -4
const val FN = -5
const val FN_LOCK = -6
//const val FN_LOCK = -6
const val DELETE = -7
const val DELETE_WORD = -8
const val FORWARD_DELETE = -9
const val FORWARD_DELETE_WORD = -10
//const val DELETE_WORD = -8
//const val FORWARD_DELETE = -9
//const val FORWARD_DELETE_WORD = -10
const val SHIFT = -11
const val CAPS_LOCK = -13
@ -51,21 +51,21 @@ object KeyCode {
const val CLIPBOARD_SELECT_WORD = -34 // CLIPBOARD_SELECT
const val CLIPBOARD_SELECT_ALL = -35
const val CLIPBOARD_CLEAR_HISTORY = -36
const val CLIPBOARD_CLEAR_FULL_HISTORY = -37
const val CLIPBOARD_CLEAR_PRIMARY_CLIP = -38
//const val CLIPBOARD_CLEAR_FULL_HISTORY = -37
//const val CLIPBOARD_CLEAR_PRIMARY_CLIP = -38
const val COMPACT_LAYOUT_TO_LEFT = -111
const val COMPACT_LAYOUT_TO_RIGHT = -112
//const val COMPACT_LAYOUT_TO_LEFT = -111
//const val COMPACT_LAYOUT_TO_RIGHT = -112
const val SPLIT_LAYOUT = -113
const val MERGE_LAYOUT = -114
//const val MERGE_LAYOUT = -114
const val UNDO = -131
const val REDO = -132
const val ALPHA = -201 // VIEW_CHARACTERS
const val SYMBOL = -202 // VIEW_SYMBOLS
const val VIEW_SYMBOLS2 = -203
const val VIEW_NUMERIC = -204
//const val VIEW_SYMBOLS2 = -203
//const val VIEW_NUMERIC = -204
const val NUMPAD = -205 // VIEW_NUMERIC_ADVANCED
const val VIEW_PHONE = -206
const val VIEW_PHONE2 = -207
@ -74,21 +74,21 @@ object KeyCode {
const val EMOJI = -212 // IME_UI_MODE_MEDIA
const val CLIPBOARD = -213 // IME_UI_MODE_CLIPBOARD
const val SYSTEM_INPUT_METHOD_PICKER = -221
const val SYSTEM_PREV_INPUT_METHOD = -222
const val SYSTEM_NEXT_INPUT_METHOD = -223
const val IME_SUBTYPE_PICKER = -224
const val IME_PREV_SUBTYPE = -225
const val IME_NEXT_SUBTYPE = -226
//const val SYSTEM_INPUT_METHOD_PICKER = -221
//const val SYSTEM_PREV_INPUT_METHOD = -222
//const val SYSTEM_NEXT_INPUT_METHOD = -223
//const val IME_SUBTYPE_PICKER = -224
//const val IME_PREV_SUBTYPE = -225
//const val IME_NEXT_SUBTYPE = -226
const val LANGUAGE_SWITCH = -227
const val IME_SHOW_UI = -231
const val IME_HIDE_UI = -232
//const val IME_SHOW_UI = -231
//const val IME_HIDE_UI = -232
const val VOICE_INPUT = -233
const val TOGGLE_SMARTBAR_VISIBILITY = -241
const val TOGGLE_ACTIONS_OVERFLOW = -242
const val TOGGLE_ACTIONS_EDITOR = -243
//const val TOGGLE_SMARTBAR_VISIBILITY = -241
//const val TOGGLE_ACTIONS_OVERFLOW = -242
//const val TOGGLE_ACTIONS_EDITOR = -243
const val TOGGLE_INCOGNITO_MODE = -244
const val TOGGLE_AUTOCORRECT = -245
@ -104,18 +104,18 @@ object KeyCode {
const val CURRENCY_SLOT_6 = -806
const val MULTIPLE_CODE_POINTS = -902
const val DRAG_MARKER = -991
const val NOOP = -999
//const val DRAG_MARKER = -991
//const val NOOP = -999
const val CHAR_WIDTH_SWITCHER = -9701
const val CHAR_WIDTH_FULL = -9702
const val CHAR_WIDTH_HALF = -9703
//const val CHAR_WIDTH_SWITCHER = -9701
//const val CHAR_WIDTH_FULL = -9702
//const val CHAR_WIDTH_HALF = -9703
const val KANA_SMALL = 12307
const val KANA_SWITCHER = -9710
const val KANA_HIRA = -9711
const val KANA_KATA = -9712
const val KANA_HALF_KATA = -9713
//const val KANA_SMALL = 12307
//const val KANA_SWITCHER = -9710
//const val KANA_HIRA = -9711
//const val KANA_KATA = -9712
//const val KANA_HALF_KATA = -9713
const val KESHIDA = 1600
const val ZWNJ = 8204 // 0x200C, named HALF_SPACE in FlorisBoard
@ -137,7 +137,7 @@ object KeyCode {
const val PAGE_UP = -10010
const val PAGE_DOWN = -10011
const val META = -10012
const val META_LOCK = -10013 // to be consistent with the CTRL/ALT/FN LOCK codes, not sure whether this will be used
//const val META_LOCK = -10013 // to be consistent with the CTRL/ALT/FN LOCK codes, not sure whether this will be used
const val TAB = -10014
const val WORD_LEFT = -10015
const val WORD_RIGHT = -10016
@ -165,8 +165,15 @@ object KeyCode {
const val F11 = -10038
const val F12 = -10039
const val BACK = -10040
const val SELECT_LEFT = -10041
const val SELECT_RIGHT = -10042
//const val SELECT_LEFT = -10041
//const val SELECT_RIGHT = -10042
const val TIMESTAMP = -10043
const val CTRL_LEFT = -10044
const val CTRL_RIGHT = -10045
const val ALT_LEFT = -10046
const val ALT_RIGHT = -10047
const val META_LEFT = -10048
const val META_RIGHT = -10049
/** to make sure a FlorisBoard code works when reading a JSON layout */
fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
@ -182,7 +189,8 @@ object KeyCode {
SYMBOL_ALPHA, TOGGLE_ONE_HANDED_MODE, SWITCH_ONE_HANDED_MODE, SPLIT_LAYOUT, SHIFT_ENTER,
ACTION_NEXT, ACTION_PREVIOUS, NOT_SPECIFIED, CLIPBOARD_COPY_ALL, WORD_LEFT, WORD_RIGHT, PAGE_UP,
PAGE_DOWN, META, TAB, ESCAPE, INSERT, SLEEP, MEDIA_PLAY, MEDIA_PAUSE, MEDIA_PLAY_PAUSE, MEDIA_NEXT,
MEDIA_PREVIOUS, VOL_UP, VOL_DOWN, MUTE, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, BACK
MEDIA_PREVIOUS, VOL_UP, VOL_DOWN, MUTE, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, BACK,
TIMESTAMP, CTRL_LEFT, CTRL_RIGHT, ALT_LEFT, ALT_RIGHT, META_LEFT, META_RIGHT
-> this
// conversion
@ -194,8 +202,12 @@ object KeyCode {
else -> throw IllegalStateException("key code $this not yet supported")
}
// todo: three are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
/** convert a keyCode / codePoint to a KeyEvent.KEYCODE_<xxx>, fallback to KeyEvent.KEYCODE_UNKNOWN */
// todo: there are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
/**
* Convert a keyCode / codePoint to a KeyEvent.KEYCODE_<xxx>.
* Fallback to KeyEvent.KEYCODE_UNKNOWN.
* To be uses for fake hardware key press.
* */
fun Int.toKeyEventCode(): Int = if (this > 0)
when (this.toChar().uppercaseChar()) {
'/' -> KeyEvent.KEYCODE_SLASH

View file

@ -31,6 +31,7 @@ object KeyLabel {
const val META = "meta"
const val TAB = "tab"
const val ESCAPE = "esc"
const val TIMESTAMP = "timestamp"
/** to make sure a FlorisBoard label works when reading a JSON layout */
// resulting special labels should be names of FunctionalKey enum, case insensitive

View file

@ -49,6 +49,6 @@ open class PopupSet<T : AbstractKeyData>(
}
class SimplePopups(val popupKeys: Collection<String>?) : PopupSet<AbstractKeyData>() {
override fun getPopupKeyLabels(params: KeyboardParams) = popupKeys
override fun getPopupKeyLabels(params: KeyboardParams) = popupKeys?.map { KeyData.processLabel(it, params) }
override fun isEmpty(): Boolean = popupKeys.isNullOrEmpty()
}

View file

@ -12,6 +12,7 @@ import kotlinx.serialization.Transient
import helium314.keyboard.keyboard.Key
import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.keyboard.internal.KeyboardCodesSet
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
@ -24,6 +25,7 @@ import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.spellcheck.AndroidSpellCheckerService
import helium314.keyboard.latin.utils.InputTypeUtils
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.ToolbarKey
import helium314.keyboard.latin.utils.getCodeForToolbarKey
@ -130,6 +132,9 @@ sealed interface KeyData : AbstractKeyData {
keys.add("!icon/start_onehanded_mode_key|!code/key_toggle_onehanded")
if (!params.mId.mDeviceLocked)
keys.add("!icon/settings_key|!code/key_settings")
if (shouldShowTldPopups(params)) {
keys.add(",")
}
return keys
}
@ -284,6 +289,40 @@ sealed interface KeyData : AbstractKeyData {
return getStringInLocale(id, params)
}
fun processLabel(label: String, params: KeyboardParams): String = when (label) {
KeyLabel.SYMBOL_ALPHA -> if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelSymbol else params.mLocaleKeyboardInfos.labelAlphabet
KeyLabel.SYMBOL -> params.mLocaleKeyboardInfos.labelSymbol
KeyLabel.ALPHA -> params.mLocaleKeyboardInfos.labelAlphabet
KeyLabel.COMMA -> params.mLocaleKeyboardInfos.labelComma
KeyLabel.PERIOD -> getPeriodLabel(params)
KeyLabel.SPACE -> getSpaceLabel(params)
KeyLabel.ACTION -> "${getActionKeyLabel(params)}|${getActionKeyCode(params)}"
KeyLabel.DELETE -> "!icon/delete_key|!code/key_delete"
KeyLabel.SHIFT -> "${getShiftLabel(params)}|!code/key_shift"
KeyLabel.COM -> params.mLocaleKeyboardInfos.tlds.first()
KeyLabel.LANGUAGE_SWITCH -> "!icon/language_switch_key|!code/key_language_switch"
KeyLabel.ZWNJ -> "!icon/zwnj_key|\u200C"
KeyLabel.CURRENCY -> params.mLocaleKeyboardInfos.currencyKey.first
KeyLabel.CURRENCY1 -> params.mLocaleKeyboardInfos.currencyKey.second[0]
KeyLabel.CURRENCY2 -> params.mLocaleKeyboardInfos.currencyKey.second[1]
KeyLabel.CURRENCY3 -> params.mLocaleKeyboardInfos.currencyKey.second[2]
KeyLabel.CURRENCY4 -> params.mLocaleKeyboardInfos.currencyKey.second[3]
KeyLabel.CURRENCY5 -> params.mLocaleKeyboardInfos.currencyKey.second[4]
KeyLabel.CTRL, KeyLabel.ALT, KeyLabel.FN, KeyLabel.META , KeyLabel.ESCAPE -> label.uppercase(Locale.US)
KeyLabel.TAB -> "!icon/tab_key|!code/${KeyCode.TAB}"
KeyLabel.TIMESTAMP -> "⌚|!code/${KeyCode.TIMESTAMP}"
else -> {
if (label in toolbarKeyStrings.values) {
"!icon/$label|!code/${getCodeForToolbarKey(ToolbarKey.valueOf(label.uppercase(Locale.US)))}"
} else label
}
}
private fun shouldShowTldPopups(params: KeyboardParams): Boolean =
(Settings.getInstance().current.mShowTldPopupKeys
&& params.mId.mSubtype.layouts[LayoutType.FUNCTIONAL] != "functional_keys_tablet"
&& params.mId.mMode in setOf(KeyboardId.MODE_URL, KeyboardId.MODE_EMAIL))
// could make arrays right away, but they need to be copied anyway as popupKeys arrays are modified when creating KeyParams
private const val POPUP_EYS_NAVIGATE_PREVIOUS = "!icon/previous_key|!code/key_action_previous,!icon/clipboard_action_key|!code/key_clipboard"
private const val POPUP_EYS_NAVIGATE_NEXT = "!icon/clipboard_action_key|!code/key_clipboard,!icon/next_key|!code/key_action_next"
@ -300,12 +339,13 @@ sealed interface KeyData : AbstractKeyData {
// so better only do it in case the popup stuff needs more improvements
// idea: directly create PopupKeySpec, but need to deal with needsToUpcase and popupKeysColumnAndFlags
fun getPopupLabel(params: KeyboardParams): String {
val newLabel = processLabel(params)
val newLabel = processLabel(label, params)
if (code == KeyCode.UNSPECIFIED) {
if (newLabel == label) return label
if (newLabel == label || newLabel.contains(KeyboardCodesSet.PREFIX_CODE))
return newLabel
val newCode = processCode()
if (newLabel.endsWith("|")) return "${newLabel}!code/$newCode" // for toolbar keys
return if (newCode == code) newLabel else "${newLabel}|!code/$newCode"
if (newLabel.endsWith("|")) return "${newLabel}${KeyboardCodesSet.PREFIX_CODE}$newCode" // maybe not used any more
return if (newCode == code) newLabel else "${newLabel}|${KeyboardCodesSet.PREFIX_CODE}$newCode"
}
if (code >= 32) {
if (newLabel.startsWith(KeyboardIconsSet.PREFIX_ICON)) {
@ -323,12 +363,12 @@ sealed interface KeyData : AbstractKeyData {
val outputText = String(codePoints, 0, codePoints.size)
return "${newLabel}|$outputText"
}
return if (newLabel.endsWith("|")) "$newLabel!code/${processCode()}" // for toolbar keys
else "$newLabel|!code/${processCode()}"
return if (newLabel.endsWith("|")) "$newLabel${KeyboardCodesSet.PREFIX_CODE}${processCode()}" // for toolbar keys
else "$newLabel|${KeyboardCodesSet.PREFIX_CODE}${processCode()}"
}
fun getCurrencyLabel(params: KeyboardParams): String {
val newLabel = processLabel(params)
val newLabel = processLabel(label, params)
return when (code) {
// consider currency codes for label
KeyCode.CURRENCY_SLOT_1 -> "$newLabel|${params.mLocaleKeyboardInfos.currencyKey.first}"
@ -387,9 +427,9 @@ sealed interface KeyData : AbstractKeyData {
newLabel = getCurrencyLabel(params)
} else {
newCode = processCode()
newLabel = processLabel(params)
newLabel = processLabel(label, params)
}
val newLabelFlags = labelFlags or additionalLabelFlags or getAdditionalLabelFlags(params)
var newLabelFlags = labelFlags or additionalLabelFlags or getAdditionalLabelFlags(params)
val newPopupKeys = popup.merge(getAdditionalPopupKeys(params))
val background = when (type) {
@ -401,6 +441,9 @@ sealed interface KeyData : AbstractKeyData {
KeyType.LOCK -> getShiftBackground(params)
null -> getDefaultBackground(params)
}
if (background == Key.BACKGROUND_TYPE_FUNCTIONAL
|| background == Key.BACKGROUND_TYPE_STICKY_ON || background == Key.BACKGROUND_TYPE_STICKY_OFF)
newLabelFlags = newLabelFlags or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
return if (newCode == KeyCode.UNSPECIFIED || newCode == KeyCode.MULTIPLE_CODE_POINTS) {
// code will be determined from label if possible (i.e. label is single code point)
@ -470,37 +513,6 @@ sealed interface KeyData : AbstractKeyData {
else params.mDefaultKeyWidth
}
// todo (later): encoding the code in the label should be avoided, because we know it already
private fun processLabel(params: KeyboardParams): String = when (label) {
KeyLabel.SYMBOL_ALPHA -> if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelSymbol else params.mLocaleKeyboardInfos.labelAlphabet
KeyLabel.SYMBOL -> params.mLocaleKeyboardInfos.labelSymbol
KeyLabel.ALPHA -> params.mLocaleKeyboardInfos.labelAlphabet
KeyLabel.COMMA -> params.mLocaleKeyboardInfos.labelComma
KeyLabel.PERIOD -> getPeriodLabel(params)
KeyLabel.SPACE -> getSpaceLabel(params)
KeyLabel.ACTION -> "${getActionKeyLabel(params)}|${getActionKeyCode(params)}"
KeyLabel.DELETE -> "!icon/delete_key|!code/key_delete"
KeyLabel.SHIFT -> "${getShiftLabel(params)}|!code/key_shift"
// KeyLabel.EMOJI -> "!icon/emoji_normal_key|!code/key_emoji"
// todo (later): label and popupKeys for .com should be in localeKeyTexts, handled similar to currency key
KeyLabel.COM -> ".com"
KeyLabel.LANGUAGE_SWITCH -> "!icon/language_switch_key|!code/key_language_switch"
KeyLabel.ZWNJ -> "!icon/zwnj_key|\u200C"
KeyLabel.CURRENCY -> params.mLocaleKeyboardInfos.currencyKey.first
KeyLabel.CURRENCY1 -> params.mLocaleKeyboardInfos.currencyKey.second[0]
KeyLabel.CURRENCY2 -> params.mLocaleKeyboardInfos.currencyKey.second[1]
KeyLabel.CURRENCY3 -> params.mLocaleKeyboardInfos.currencyKey.second[2]
KeyLabel.CURRENCY4 -> params.mLocaleKeyboardInfos.currencyKey.second[3]
KeyLabel.CURRENCY5 -> params.mLocaleKeyboardInfos.currencyKey.second[4]
KeyLabel.CTRL, KeyLabel.ALT, KeyLabel.FN, KeyLabel.META , KeyLabel.ESCAPE -> label.uppercase(Locale.US)
KeyLabel.TAB -> "!icon/tab_key|"
else -> {
if (label in toolbarKeyStrings.values) {
"!icon/$label|"
} else label
}
}
private fun processCode(): Int {
if (code != KeyCode.UNSPECIFIED) return code
return when (label) {
@ -513,6 +525,7 @@ sealed interface KeyData : AbstractKeyData {
KeyLabel.META -> KeyCode.META
KeyLabel.TAB -> KeyCode.TAB
KeyLabel.ESCAPE -> KeyCode.ESCAPE
KeyLabel.TIMESTAMP -> KeyCode.TIMESTAMP
else -> {
if (label in toolbarKeyStrings.values) {
getCodeForToolbarKey(ToolbarKey.valueOf(label.uppercase(Locale.US)))
@ -524,17 +537,20 @@ sealed interface KeyData : AbstractKeyData {
// todo (later): add explanations / reasoning, often this is just taken from conversion from OpenBoard / AOSP layouts
private fun getAdditionalLabelFlags(params: KeyboardParams): Int {
return when (label) {
KeyLabel.ALPHA, KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL -> Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR
KeyLabel.ALPHA, KeyLabel.SYMBOL_ALPHA, KeyLabel.SYMBOL -> Key.LABEL_FLAGS_PRESERVE_CASE
KeyLabel.COMMA -> Key.LABEL_FLAGS_HAS_POPUP_HINT
// essentially this only changes the appearance of the armenian period key in holo theme
KeyLabel.PERIOD -> Key.LABEL_FLAGS_HAS_POPUP_HINT and if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelFlags else 0
// essentially the first term only changes the appearance of the armenian period key in holo theme
KeyLabel.PERIOD -> (Key.LABEL_FLAGS_HAS_POPUP_HINT and
if (params.mId.isAlphabetKeyboard) params.mLocaleKeyboardInfos.labelFlags else 0) or
Key.LABEL_FLAGS_PRESERVE_CASE or
// in functional_keys.json the label flag is already defined, let's not override it in case it's removed by the user
if (!params.mId.isAlphaOrSymbolKeyboard && shouldShowTldPopups(params)) Key.LABEL_FLAGS_DISABLE_HINT_LABEL else 0
KeyLabel.ACTION -> {
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_AUTO_X_SCALE or
Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO or Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR or
Key.LABEL_FLAGS_PRESERVE_CASE or Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO or
Key.LABEL_FLAGS_HAS_POPUP_HINT or KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId)
}
KeyLabel.SPACE -> if (params.mId.isNumberLayout) Key.LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM else 0
KeyLabel.SHIFT -> Key.LABEL_FLAGS_PRESERVE_CASE or if (!params.mId.isAlphabetKeyboard) Key.LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR else 0
KeyLabel.SHIFT -> Key.LABEL_FLAGS_PRESERVE_CASE
toolbarKeyStrings[ToolbarKey.EMOJI] -> KeyboardTheme.getThemeActionAndEmojiKeyLabelFlags(params.mThemeId)
KeyLabel.COM -> Key.LABEL_FLAGS_AUTO_X_SCALE or Key.LABEL_FLAGS_FONT_NORMAL or Key.LABEL_FLAGS_HAS_POPUP_HINT or Key.LABEL_FLAGS_PRESERVE_CASE
KeyLabel.ZWNJ -> Key.LABEL_FLAGS_HAS_POPUP_HINT
@ -546,12 +562,12 @@ sealed interface KeyData : AbstractKeyData {
private fun getAdditionalPopupKeys(params: KeyboardParams): PopupSet<AbstractKeyData>? {
if (groupId == GROUP_COMMA) return SimplePopups(getCommaPopupKeys(params))
if (groupId == GROUP_PERIOD) return SimplePopups(getPunctuationPopupKeys(params))
if (groupId == GROUP_PERIOD) return getPeriodPopups(params)
if (groupId == GROUP_ENTER) return getActionKeyPopupKeys(params)
if (groupId == GROUP_NO_DEFAULT_POPUP) return null
return when (label) {
KeyLabel.COMMA -> SimplePopups(getCommaPopupKeys(params))
KeyLabel.PERIOD -> SimplePopups(getPunctuationPopupKeys(params))
KeyLabel.PERIOD -> getPeriodPopups(params)
KeyLabel.ACTION -> getActionKeyPopupKeys(params)
KeyLabel.SHIFT -> {
if (params.mId.isAlphabetKeyboard) SimplePopups(
@ -561,13 +577,22 @@ sealed interface KeyData : AbstractKeyData {
)
) else null // why the alphabet popup keys actually?
}
KeyLabel.COM -> SimplePopups(listOf(Key.POPUP_KEYS_HAS_LABELS, ".net", ".org", ".gov", ".edu"))
KeyLabel.COM -> SimplePopups(
listOf(Key.POPUP_KEYS_HAS_LABELS).plus(params.mLocaleKeyboardInfos.tlds.drop(1))
)
KeyLabel.ZWNJ -> SimplePopups(listOf("!icon/zwj_key|\u200D"))
// only add currency popups if there are none defined on the key
KeyLabel.CURRENCY -> if (popup.isEmpty()) SimplePopups(params.mLocaleKeyboardInfos.currencyKey.second) else null
else -> null
}
}
private fun getPeriodPopups(params: KeyboardParams): SimplePopups =
SimplePopups(
if (shouldShowTldPopups(params)) params.mLocaleKeyboardInfos.tlds
else getPunctuationPopupKeys(params)
)
}
/**

View file

@ -6,6 +6,7 @@ import android.content.Context
import androidx.core.content.edit
import helium314.keyboard.keyboard.ColorSetting
import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.keyboard.emoji.SupportedEmojis
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
import helium314.keyboard.latin.common.ColorType
import helium314.keyboard.latin.common.Constants.Separators
@ -23,6 +24,7 @@ import helium314.keyboard.latin.utils.DictionaryInfoUtils.USER_DICTIONARY_SUFFIX
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutType.Companion.folder
import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.ScriptUtils.SCRIPT_LATIN
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SubtypeSettings
@ -32,8 +34,8 @@ import helium314.keyboard.latin.utils.defaultPinnedToolbarPref
import helium314.keyboard.latin.utils.getResourceSubtypes
import helium314.keyboard.latin.utils.locale
import helium314.keyboard.latin.utils.mainLayoutName
import helium314.keyboard.latin.utils.mainLayoutNameOrQwerty
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.latin.utils.protectedPrefs
import helium314.keyboard.latin.utils.upgradeToolbarPrefs
import helium314.keyboard.latin.utils.writeCustomKeyCodes
import helium314.keyboard.settings.screens.colorPrefsAndResIds
@ -51,6 +53,16 @@ class App : Application() {
checkVersionUpgrade(this)
app = this
Defaults.initDynamicDefaults(this)
LayoutUtilsCustom.removeMissingLayouts(this) // only after version upgrade
SupportedEmojis.load(this)
val packageInfo = packageManager.getPackageInfo(packageName, 0)
@Suppress("DEPRECATION")
Log.i(
"startup", "Starting ${applicationInfo.processName} version ${packageInfo.versionName} (${
packageInfo.versionCode
}) on Android ${android.os.Build.VERSION.RELEASE} (SDK ${android.os.Build.VERSION.SDK_INT})"
)
}
companion object {
@ -74,16 +86,13 @@ fun checkVersionUpgrade(context: Context) {
if (oldVersion == BuildConfig.VERSION_CODE)
return
// clear extracted dictionaries, in case updated version contains newer ones
DictionaryInfoUtils.getCachedDirectoryList(context)?.forEach {
if (!it.isDirectory) return@forEach
DictionaryInfoUtils.getCacheDirectories(context).forEach {
val files = it.listFiles() ?: return@forEach
for (file in files) {
if (!file.name.endsWith(USER_DICTIONARY_SUFFIX))
file.delete()
}
}
if (oldVersion == 0) // new install or restoring settings from old app name
upgradesWhenComingFromOldAppName(context)
if (oldVersion <= 1000) { // upgrade old custom layouts name
val oldShiftSymbolsFile = getCustomLayoutFile("custom.shift_symbols", context)
if (oldShiftSymbolsFile.exists()) {
@ -161,7 +170,7 @@ fun checkVersionUpgrade(context: Context) {
split[1] = newName
split.joinToString(":")
}
Settings.writePrefAdditionalSubtypes(prefs, newSubtypeStrings.joinToString(";"))
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, newSubtypeStrings.joinToString(";")).apply()
}
// rename other custom layouts
LayoutUtilsCustom.onLayoutFileChanged()
@ -232,7 +241,7 @@ fun checkVersionUpgrade(context: Context) {
KeyboardTheme.writeUserMoreColors(prefs, themeNameNight, moreColorsNight)
}
if (prefs.contains("theme_dark_color_all_colors")) {
val allColorsNight = readAllColorsMap(false)
val allColorsNight = readAllColorsMap(true)
prefs.edit().remove("theme_dark_color_all_colors").apply()
KeyboardTheme.writeUserAllColors(prefs, themeNameNight, allColorsNight)
}
@ -437,19 +446,19 @@ fun checkVersionUpgrade(context: Context) {
val mainLayoutName = oldSplit[1]
// we now need more information than just locale and main layout name, get it from existing subtypes
val filtered = additionalSubtypes.filter {
it.locale().toLanguageTag() == languageTag && (it.mainLayoutName() ?: "qwerty") == mainLayoutName
it.locale().toLanguageTag() == languageTag && (it.mainLayoutNameOrQwerty()) == mainLayoutName
}
if (filtered.isNotEmpty())
return@joinToString filtered.first().toSettingsSubtype().toPref()
// find best matching resource subtype
val goodMatch = resourceSubtypes.filter {
it.locale().toLanguageTag() == languageTag && (it.mainLayoutName() ?: "qwerty") == mainLayoutName
it.locale().toLanguageTag() == languageTag && (it.mainLayoutNameOrQwerty()) == mainLayoutName
}
if (goodMatch.isNotEmpty())
return@joinToString goodMatch.first().toSettingsSubtype().toPref()
// not sure how we can get here, but better deal with it
val okMatch = resourceSubtypes.filter {
it.locale().language == languageTag.constructLocale().language && (it.mainLayoutName() ?: "qwerty") == mainLayoutName
it.locale().language == languageTag.constructLocale().language && (it.mainLayoutNameOrQwerty()) == mainLayoutName
}
if (okMatch.isNotEmpty())
okMatch.first().toSettingsSubtype().toPref()
@ -530,115 +539,30 @@ fun checkVersionUpgrade(context: Context) {
prefs.edit().remove("auto_correction_confidence").putFloat(Settings.PREF_AUTO_CORRECT_THRESHOLD, value).apply()
}
}
if (oldVersion <= 2310) {
listOf(
Settings.PREF_ENABLED_SUBTYPES,
Settings.PREF_SELECTED_SUBTYPE,
Settings.PREF_ADDITIONAL_SUBTYPES
).forEach { key ->
val value = prefs.getString(key, "")!!
if ("bengali," in value) {
prefs.edit().putString(key, value.replace("bengali,", "bengali_inscript,")).apply()
if (oldVersion <= 2310) {
listOf(
Settings.PREF_ENABLED_SUBTYPES,
Settings.PREF_SELECTED_SUBTYPE,
Settings.PREF_ADDITIONAL_SUBTYPES
).forEach { key ->
val value = prefs.getString(key, "")!!
if ("bengali," in value) {
prefs.edit().putString(key, value.replace("bengali,", "bengali_inscript,")).apply()
}
}
}
if (oldVersion <= 3001 && prefs.getInt(Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME, Defaults.PREF_CLIPBOARD_HISTORY_RETENTION_TIME) <= 0) {
prefs.edit().putInt(Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME, 121).apply()
}
if (oldVersion <= 3002) {
prefs.all.filterKeys { it.startsWith(Settings.PREF_USER_ALL_COLORS_PREFIX) }.forEach {
val oldValue = prefs.getString(it.key, "")!!
if ("KEY_PREVIEW" !in oldValue) return@forEach
val newValue = oldValue.replace("KEY_PREVIEW", "KEY_PREVIEW_BACKGROUND")
prefs.edit().putString(it.key, newValue).apply()
}
}
}
upgradeToolbarPrefs(prefs)
LayoutUtilsCustom.onLayoutFileChanged() // just to be sure
prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) }
}
// todo (later): remove it when most users probably have upgraded
private fun upgradesWhenComingFromOldAppName(context: Context) {
// move layout files
try {
File(context.filesDir, "layouts").listFiles()?.forEach {
it.copyTo(getCustomLayoutFile(it.name, context), true)
it.delete()
}
} catch (_: Exception) {}
// move background images
try {
val bgDay = File(context.filesDir, "custom_background_image")
if (bgDay.isFile) {
bgDay.copyTo(Settings.getCustomBackgroundFile(context, false, false), true)
bgDay.delete()
}
val bgNight = File(context.filesDir, "custom_background_image_night")
if (bgNight.isFile) {
bgNight.copyTo(Settings.getCustomBackgroundFile(context, true, false), true)
bgNight.delete()
}
} catch (_: Exception) {}
// upgrade prefs
val prefs = context.prefs()
if (prefs.all.containsKey("theme_variant")) {
prefs.edit().putString(Settings.PREF_THEME_COLORS, prefs.getString("theme_variant", "")).apply()
prefs.edit().remove("theme_variant").apply()
}
if (prefs.all.containsKey("theme_variant_night")) {
prefs.edit().putString(Settings.PREF_THEME_COLORS_NIGHT, prefs.getString("theme_variant_night", "")).apply()
prefs.edit().remove("theme_variant_night").apply()
}
prefs.all.toMap().forEach {
if (it.key.startsWith("pref_key_") && it.key != "pref_key_longpress_timeout") {
var remove = true
when (val value = it.value) {
is Boolean -> prefs.edit().putBoolean(it.key.substringAfter("pref_key_"), value).apply()
is Int -> prefs.edit().putInt(it.key.substringAfter("pref_key_"), value).apply()
is Long -> prefs.edit().putLong(it.key.substringAfter("pref_key_"), value).apply()
is String -> prefs.edit().putString(it.key.substringAfter("pref_key_"), value).apply()
is Float -> prefs.edit().putFloat(it.key.substringAfter("pref_key_"), value).apply()
else -> remove = false
}
if (remove)
prefs.edit().remove(it.key).apply()
} else if (it.key.startsWith("pref_")) {
var remove = true
when (val value = it.value) {
is Boolean -> prefs.edit().putBoolean(it.key.substringAfter("pref_"), value).apply()
is Int -> prefs.edit().putInt(it.key.substringAfter("pref_"), value).apply()
is Long -> prefs.edit().putLong(it.key.substringAfter("pref_"), value).apply()
is String -> prefs.edit().putString(it.key.substringAfter("pref_"), value).apply()
is Float -> prefs.edit().putFloat(it.key.substringAfter("pref_"), value).apply()
else -> remove = false
}
if (remove)
prefs.edit().remove(it.key).apply()
}
}
// change more_keys to popup_keys
if (prefs.contains("more_keys_order")) {
prefs.edit().putString(Settings.PREF_POPUP_KEYS_ORDER, prefs.getString("more_keys_order", "")?.replace("more_", "popup_")).apply()
prefs.edit().remove("more_keys_order").apply()
}
if (prefs.contains("more_keys_labels_order")) {
prefs.edit().putString(Settings.PREF_POPUP_KEYS_LABELS_ORDER, prefs.getString("more_keys_labels_order", "")?.replace("more_", "popup_")).apply()
prefs.edit().remove("more_keys_labels_order").apply()
}
if (prefs.contains("more_more_keys")) {
prefs.edit().putString(Settings.PREF_MORE_POPUP_KEYS, prefs.getString("more_more_keys", "")).apply()
prefs.edit().remove("more_more_keys").apply()
}
if (prefs.contains("spellcheck_use_contacts")) {
prefs.edit().putBoolean(Settings.PREF_USE_CONTACTS, prefs.getBoolean("spellcheck_use_contacts", false)).apply()
prefs.edit().remove("spellcheck_use_contacts").apply()
}
// upgrade additional subtype locale strings
if (prefs.contains(Settings.PREF_ADDITIONAL_SUBTYPES)) {
val additionalSubtypes = mutableListOf<String>()
prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, "")!!.split(";").forEach {
val localeString = it.substringBefore(":")
additionalSubtypes.add(it.replace(localeString, localeString.constructLocale().toLanguageTag()))
}
Settings.writePrefAdditionalSubtypes(prefs, additionalSubtypes.joinToString(";"))
}
// move pinned clips to credential protected storage if device is not locked (should never happen)
if (!prefs.contains(Settings.PREF_PINNED_CLIPS)) return
try {
val defaultProtectedPrefs = context.protectedPrefs()
defaultProtectedPrefs.edit { putString(Settings.PREF_PINNED_CLIPS, prefs.getString(Settings.PREF_PINNED_CLIPS, "")) }
prefs.edit { remove(Settings.PREF_PINNED_CLIPS) }
} catch (_: IllegalStateException) {
// SharedPreferences in credential encrypted storage are not available until after user is unlocked
}
}

View file

@ -0,0 +1,92 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin;
import android.content.Context;
import com.android.inputmethod.latin.BinaryDictionary;
import java.io.File;
import java.util.Locale;
import helium314.keyboard.latin.common.StringUtils;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.SpacedTokens;
public class AppsBinaryDictionary extends ExpandableBinaryDictionary {
private static final String TAG = AppsBinaryDictionary.class.getSimpleName();
private static final String NAME = "apps";
private static final int FREQUENCY_FOR_APPS = 100;
private static final int FREQUENCY_FOR_APPS_BIGRAM = 200;
private static final boolean DEBUG = false;
private static final boolean DEBUG_DUMP = false;
private final AppsManager mAppsManager;
protected AppsBinaryDictionary(final Context ctx, final Locale locale,
final File dictFile, final String name) {
super(ctx, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_APPS, dictFile);
mAppsManager = new AppsManager(ctx);
reloadDictionaryIfRequired();
}
public static AppsBinaryDictionary getDictionary(final Context context, final Locale locale,
final File dictFile, final String dictNamePrefix) {
return new AppsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME);
}
/**
* Typically called whenever the dictionary is created for the first time or recreated when we
* think that there are updates to the dictionary. This is called asynchronously.
*/
@Override
public void loadInitialContentsLocked() {
loadDictionaryLocked();
}
/**
* Loads app names to the dictionary.
*/
private void loadDictionaryLocked() {
for (final String name : mAppsManager.getNames()) {
addNameLocked(name);
}
}
/**
* Adds the words in an app label to the binary dictionary along with their n-grams.
*/
private void addNameLocked(final String appLabel) {
NgramContext ngramContext = NgramContext.getEmptyPrevWordsContext(
BinaryDictionary.MAX_PREV_WORD_COUNT_FOR_N_GRAM);
// TODO: Better tokenization for non-Latin writing systems
for (final String word : new SpacedTokens(appLabel)) {
if (DEBUG_DUMP) {
Log.d(TAG, "addName word = " + word);
}
final int wordLen = StringUtils.codePointCount(word);
// Don't add single letter words, possibly confuses capitalization of i.
if (1 < wordLen && wordLen <= MAX_WORD_LENGTH) {
if (DEBUG) {
Log.d(TAG, "addName " + appLabel + ", " + word + ", " + ngramContext);
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addUnigramLocked(word, FREQUENCY_FOR_APPS,
null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */,
false /* isPossiblyOffensive */,
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
if (ngramContext.isValid()) {
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addNgramEntryLocked(ngramContext,
word,
FREQUENCY_FOR_APPS_BIGRAM,
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
}
ngramContext = ngramContext.getNextNgramContext(
new NgramContext.WordInfo(word));
}
}
}
}

View file

@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import java.util.HashSet
class AppsManager(context: Context) {
private val mPackageManager: PackageManager = context.packageManager
/**
* Returns all app labels associated with a launcher icon, sorted arbitrarily.
*/
fun getNames(): HashSet<String> {
val filter = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
// activities with an entry/icon for the launcher
val launcherApps: List<ResolveInfo> = mPackageManager.queryIntentActivities(filter, 0)
val names = HashSet<String>(launcherApps.size)
for (info in launcherApps) {
val name = info.activityInfo.loadLabel(mPackageManager).toString()
names.add(name)
}
return names
}
}

View file

@ -2,18 +2,12 @@
package helium314.keyboard.latin
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Serializable
data class ClipboardHistoryEntry (
var timeStamp: Long,
@Serializable(with = CharSequenceStringSerializer::class)
val content: CharSequence,
val content: String,
var isPinned: Boolean = false
) : Comparable<ClipboardHistoryEntry> {
override fun compareTo(other: ClipboardHistoryEntry): Int {
@ -21,13 +15,3 @@ data class ClipboardHistoryEntry (
return if (result != 0) result else other.timeStamp.compareTo(timeStamp)
}
}
class CharSequenceStringSerializer : KSerializer<CharSequence> {
override val descriptor = PrimitiveSerialDescriptor("CharSequence", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: CharSequence) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder) = decoder.decodeString()
}

View file

@ -61,7 +61,7 @@ class ClipboardHistoryManager(
val content = clipItem.coerceToText(latinIME)
if (TextUtils.isEmpty(content)) return
val duplicateEntryIndex = historyEntries.indexOfFirst { it.content.toString() == content.toString() }
val duplicateEntryIndex = historyEntries.indexOfFirst { it.content == content.toString() }
if (duplicateEntryIndex != -1) {
val existingEntry = historyEntries[duplicateEntryIndex]
if (existingEntry.timeStamp == timeStamp) return // nothing to change (may occur frequently starting with API 30)
@ -74,9 +74,9 @@ class ClipboardHistoryManager(
onHistoryChangeListener?.onClipboardHistoryEntryMoved(duplicateEntryIndex, newIndex)
return
}
if (historyEntries.any { it.content.toString() == content.toString() }) return
if (historyEntries.any { it.content == content.toString() }) return
val entry = ClipboardHistoryEntry(timeStamp, content)
val entry = ClipboardHistoryEntry(timeStamp, content.toString())
historyEntries.add(entry)
sortHistoryEntries()
val at = historyEntries.indexOf(entry)
@ -120,7 +120,7 @@ class ClipboardHistoryManager(
private fun checkClipRetentionElapsed() {
val mins = latinIME.mSettings.current.mClipboardHistoryRetentionTime
if (mins <= 0) return // No retention limit
if (mins > 120) return // No retention limit, changed from <= 0 because we want it to be larger than all other choices
val maxClipRetentionTime = mins * 60 * 1000L
val now = System.currentTimeMillis()
historyEntries.removeAll { !it.isPinned && (now - it.timeStamp) > maxClipRetentionTime }

View file

@ -14,7 +14,6 @@ import android.provider.ContactsContract.Contacts;
import helium314.keyboard.latin.utils.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.inputmethod.latin.BinaryDictionary;
@ -51,7 +50,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary
}
public static ContactsBinaryDictionary getDictionary(final Context context, @NonNull final Locale locale,
final File dictFile, final String dictNamePrefix, @Nullable final String account) {
final File dictFile, final String dictNamePrefix) {
return new ContactsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME);
}

View file

@ -20,8 +20,8 @@ public class ContactsDictionaryConstants {
/**
* Frequency for contacts information into the dictionary
*/
public static final int FREQUENCY_FOR_CONTACTS = 40;
public static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
public static final int FREQUENCY_FOR_CONTACTS = 100; // much increased from original frequency because contacts were barely suggested
public static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 200; // todo: seems broken, how to actually get bigrams?
/**
* Do not attempt to query contacts if there are more than this many entries.

View file

@ -6,15 +6,15 @@
package helium314.keyboard.latin;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Locale;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
/**
* Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
* strokes.
@ -48,6 +48,7 @@ public abstract class Dictionary {
// phony dictionary instances for them.
public static final String TYPE_MAIN = "main";
public static final String TYPE_CONTACTS = "contacts";
public static final String TYPE_APPS = "apps";
// User dictionary, the system-managed one.
public static final String TYPE_USER = "user";
// User history dictionary internal to LatinIME.
@ -56,16 +57,6 @@ public abstract class Dictionary {
// The locale for this dictionary. May be null if unknown (phony dictionary for example).
public final Locale mLocale;
/**
* Set out of the dictionary types listed above that are based on data specific to the user,
* e.g., the user's contacts.
*/
private static final HashSet<String> sUserSpecificDictionaryTypes = new HashSet<>(Arrays.asList(
TYPE_USER_TYPED,
TYPE_USER,
TYPE_CONTACTS,
TYPE_USER_HISTORY));
public Dictionary(final String dictType, final Locale locale) {
mDictType = dictType;
mLocale = locale;
@ -178,7 +169,14 @@ public abstract class Dictionary {
* @return Whether this dictionary is specific to the user.
*/
public boolean isUserSpecific() {
return sUserSpecificDictionaryTypes.contains(mDictType);
return switch (mDictType) {
case TYPE_USER_TYPED,
TYPE_USER,
TYPE_CONTACTS,
TYPE_APPS,
TYPE_USER_HISTORY -> true;
default -> false;
};
}
/**

View file

@ -6,30 +6,35 @@
package helium314.keyboard.latin;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import helium314.keyboard.latin.utils.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Class for a collection of dictionaries that behave like one dictionary.
*/
public final class DictionaryCollection extends Dictionary {
private final String TAG = DictionaryCollection.class.getSimpleName();
private final CopyOnWriteArrayList<Dictionary> mDictionaries;
private final ArrayList<Dictionary> mDictionaries;
private final float[] mWeights;
public DictionaryCollection(final String dictType, final Locale locale,
final Collection<Dictionary> dictionaries) {
final Collection<Dictionary> dictionaries, final float[] weights) {
super(dictType, locale);
mDictionaries = new CopyOnWriteArrayList<>(dictionaries);
mDictionaries = new ArrayList<>(dictionaries);
mDictionaries.removeAll(Collections.singleton(null));
if (mDictionaries.size() > weights.length) {
mWeights = new float[mDictionaries.size()];
Arrays.fill(mWeights, 1f);
Log.w(TAG, "got weights array of length " + weights.length + ", expected "+mDictionaries.size());
} else mWeights = weights;
}
@Override
@ -38,19 +43,19 @@ public final class DictionaryCollection extends Dictionary {
final SettingsValuesForSuggestion settingsValuesForSuggestion,
final int sessionId, final float weightForLocale,
final float[] inOutWeightOfLangModelVsSpatialModel) {
final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries;
final ArrayList<Dictionary> dictionaries = mDictionaries;
if (dictionaries.isEmpty()) return null;
// To avoid creating unnecessary objects, we get the list out of the first
// dictionary and add the rest to it if not null, hence the get(0)
ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composedData,
ngramContext, proximityInfoHandle, settingsValuesForSuggestion, sessionId,
weightForLocale, inOutWeightOfLangModelVsSpatialModel);
weightForLocale * mWeights[0], inOutWeightOfLangModelVsSpatialModel);
if (null == suggestions) suggestions = new ArrayList<>();
final int length = dictionaries.size();
for (int i = 1; i < length; ++ i) {
final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(
composedData, ngramContext, proximityInfoHandle, settingsValuesForSuggestion,
sessionId, weightForLocale, inOutWeightOfLangModelVsSpatialModel);
sessionId, weightForLocale * mWeights[i], inOutWeightOfLangModelVsSpatialModel);
if (null != sugg) suggestions.addAll(sugg);
}
return suggestions;
@ -93,22 +98,4 @@ public final class DictionaryCollection extends Dictionary {
for (final Dictionary dict : mDictionaries)
dict.close();
}
// Warning: this is not thread-safe. Take necessary precaution when calling.
public void addDictionary(final Dictionary newDict) {
if (null == newDict) return;
if (mDictionaries.contains(newDict)) {
Log.w(TAG, "This collection already contains this dictionary: " + newDict);
}
mDictionaries.add(newDict);
}
// Warning: this is not thread-safe. Take necessary precaution when calling.
public void removeDictionary(final Dictionary dict) {
if (mDictionaries.contains(dict)) {
mDictionaries.remove(dict);
} else {
Log.w(TAG, "This collection does not contain this dictionary: " + dict);
}
}
}

View file

@ -23,7 +23,7 @@ import java.util.concurrent.TimeUnit;
/**
* Interface that facilitates interaction with different kinds of dictionaries. Provides APIs to
* instantiate and select the correct dictionaries (based on language or account), update entries
* instantiate and select the correct dictionaries (based on language and settings), update entries
* and fetch suggestions. Currently AndroidSpellCheckerService and LatinIME both use
* DictionaryFacilitator as a client for interacting with dictionaries.
*/
@ -32,22 +32,20 @@ public interface DictionaryFacilitator {
String[] ALL_DICTIONARY_TYPES = new String[] {
Dictionary.TYPE_MAIN,
Dictionary.TYPE_CONTACTS,
Dictionary.TYPE_APPS,
Dictionary.TYPE_USER_HISTORY,
Dictionary.TYPE_USER};
String[] DYNAMIC_DICTIONARY_TYPES = new String[] {
Dictionary.TYPE_CONTACTS,
Dictionary.TYPE_APPS,
Dictionary.TYPE_USER_HISTORY,
Dictionary.TYPE_USER};
/**
* The facilitator will put words into the cache whenever it decodes them.
*/
/** The facilitator will put words into the cache whenever it decodes them. */
void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache);
/**
* The facilitator will get words from the cache whenever it needs to check their spelling.
*/
/** The facilitator will get words from the cache whenever it needs to check their spelling. */
void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache);
/**
@ -77,56 +75,64 @@ public interface DictionaryFacilitator {
*/
void onFinishInput(Context context);
/** whether a dictionary is set */
boolean isActive();
/** the locale provided in resetDictionaries */
@NonNull Locale getMainLocale();
// useful for multilingual typing
Locale getCurrentLocale();
/** the most "trusted" locale, differs from getMainLocale only if multilingual typing is used */
@NonNull Locale getCurrentLocale();
boolean usesSameSettings(
@NonNull final List<Locale> locales,
final boolean contacts,
final boolean personalization,
@Nullable final String account
final boolean apps,
final boolean personalization
);
String getAccount();
/** switches to newLocale, gets secondary locales from current settings, and sets secondary dictionaries */
void resetDictionaries(
final Context context,
final Locale newLocale,
final boolean useContactsDict,
final boolean useAppsDict,
final boolean usePersonalizedDicts,
final boolean forceReloadMainDictionary,
@Nullable final String account,
final String dictNamePrefix,
@Nullable final DictionaryInitializationListener listener);
/** removes the word from all editable dictionaries, and adds it to a blacklist in case it's in a read-only dictionary */
void removeWord(String word);
void closeDictionaries();
// The main dictionaries are loaded asynchronously. Don't cache the return value
// of these methods.
/** main dictionaries are loaded asynchronously after resetDictionaries */
boolean hasAtLeastOneInitializedMainDictionary();
/** main dictionaries are loaded asynchronously after resetDictionaries */
boolean hasAtLeastOneUninitializedMainDictionary();
/** main dictionaries are loaded asynchronously after resetDictionaries */
void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit)
throws InterruptedException;
/** adds the word to user history dictionary, calls adjustConfindences, and might add it to personal dictionary if the setting is enabled */
void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized,
@NonNull final NgramContext ngramContext, final long timeStampInSeconds,
final boolean blockPotentiallyOffensive);
/** adjust confidences for multilingual typing */
void adjustConfidences(final String word, final boolean wasAutoCapitalized);
/** a string with all used locales and their current confidences, null if multilingual typing is not used */
@Nullable String localesAndConfidences();
/** completely removes the word from user history (currently not if event is a backspace event) */
void unlearnFromUserHistory(final String word,
@NonNull final NgramContext ngramContext, final long timeStampInSeconds,
final int eventType);
// TODO: Revise the way to fusion suggestion results.
@NonNull SuggestionResults getSuggestionResults(final ComposedData composedData,
final NgramContext ngramContext, @NonNull final Keyboard keyboard,
final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId,
@ -136,12 +142,10 @@ public interface DictionaryFacilitator {
boolean isValidSuggestionWord(final String word);
boolean clearUserHistoryDictionary(final Context context);
void clearUserHistoryDictionary(final Context context);
String dump(final Context context);
String localesAndConfidences();
void dumpDictionaryForDebug(final String dictName);
@NonNull List<DictionaryStats> getDictionaryStats(final Context context);

View file

@ -0,0 +1,820 @@
/*
* Copyright (C) 2013 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.latin
import android.Manifest
import android.content.Context
import android.provider.UserDictionary
import android.util.LruCache
import helium314.keyboard.keyboard.Keyboard
import helium314.keyboard.keyboard.emoji.SupportedEmojis
import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
import helium314.keyboard.latin.NgramContext.WordInfo
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
import helium314.keyboard.latin.common.ComposedData
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.common.decapitalize
import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.permissions.PermissionsUtil
import helium314.keyboard.latin.personalization.UserHistoryDictionary
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.SuggestionResults
import helium314.keyboard.latin.utils.getSecondaryLocales
import helium314.keyboard.latin.utils.locale
import helium314.keyboard.latin.utils.prefs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.IOException
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* Facilitates interaction with different kinds of dictionaries. Provides APIs
* to instantiate and select the correct dictionaries (based on language and settings),
* update entries and fetch suggestions.
*
*
* Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as
* a client for interacting with dictionaries.
*/
class DictionaryFacilitatorImpl : DictionaryFacilitator {
private var dictionaryGroups = listOf(DictionaryGroup())
@Volatile
private var mLatchForWaitingLoadingMainDictionaries = CountDownLatch(0)
// The library does not deal well with ngram history for auto-capitalized words, so we adjust
// the ngram context to store next word suggestions for such cases.
// todo: this is awful, find a better solution / workaround
// or remove completely? not sure if it's actually an improvement
// should be fixed in the library, but that's not feasible with current user-provides-library approach
// added in 12cbd43bda7d0f0cd73925e9cf836de751c32ed0 / https://github.com/Helium314/HeliBoard/issues/135
private var tryChangingWords = false
private var changeFrom = ""
private var changeTo = ""
// todo: write cache never set, and never read (only written)
// tried to use read cache for a while, but small performance improvements are not worth the work,
// see https://github.com/Helium314/HeliBoard/issues/307
private var mValidSpellingWordReadCache: LruCache<String, Boolean>? = null
private var mValidSpellingWordWriteCache: LruCache<String, Boolean>? = null
private val scope = CoroutineScope(Dispatchers.Default)
override fun setValidSpellingWordReadCache(cache: LruCache<String, Boolean>) {
mValidSpellingWordReadCache = cache
}
override fun setValidSpellingWordWriteCache(cache: LruCache<String, Boolean>) {
mValidSpellingWordWriteCache = cache
}
// judging by usage before adding multilingual typing, this should check primary group locale only
override fun isForLocale(locale: Locale?): Boolean {
return locale != null && locale == dictionaryGroups[0].locale
}
override fun onStartInput() {
}
override fun onFinishInput(context: Context) {
for (dictGroup in dictionaryGroups) {
DictionaryFacilitator.ALL_DICTIONARY_TYPES.forEach { dictGroup.getDict(it)?.onFinishInput() }
}
}
override fun isActive(): Boolean {
return dictionaryGroups[0].locale.language.isNotEmpty()
}
override fun getMainLocale(): Locale {
return dictionaryGroups[0].locale
}
override fun getCurrentLocale(): Locale {
return currentlyPreferredDictionaryGroup.locale
}
override fun usesSameSettings(locales: List<Locale>, contacts: Boolean, apps: Boolean, personalization: Boolean): Boolean {
val dictGroup = dictionaryGroups[0] // settings are the same for all groups
return contacts == dictGroup.hasDict(Dictionary.TYPE_CONTACTS)
&& apps == dictGroup.hasDict(Dictionary.TYPE_APPS)
&& personalization == dictGroup.hasDict(Dictionary.TYPE_USER_HISTORY)
&& locales.size == dictionaryGroups.size
&& locales.none { findDictionaryGroupWithLocale(dictionaryGroups, it) == null }
}
// -------------- managing (loading & closing) dictionaries ------------
override fun resetDictionaries(
context: Context,
newLocale: Locale,
useContactsDict: Boolean,
useAppsDict: Boolean,
usePersonalizedDicts: Boolean,
forceReloadMainDictionary: Boolean,
dictNamePrefix: String,
listener: DictionaryInitializationListener?
) {
Log.i(TAG, "resetDictionaries, force reloading main dictionary: $forceReloadMainDictionary")
val locales = getUsedLocales(newLocale, context)
val subDictTypesToUse = listOfNotNull(
Dictionary.TYPE_USER,
if (useAppsDict) Dictionary.TYPE_APPS else null,
if (usePersonalizedDicts) Dictionary.TYPE_USER_HISTORY else null,
if (useContactsDict && PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
Dictionary.TYPE_CONTACTS else null
)
val (newDictionaryGroups, existingDictsToCleanup) =
getNewDictGroupsAndDictsToCleanup(locales, subDictTypesToUse, forceReloadMainDictionary, dictNamePrefix, context)
// Replace Dictionaries.
val oldDictionaryGroups: List<DictionaryGroup>
synchronized(this) {
oldDictionaryGroups = dictionaryGroups
dictionaryGroups = newDictionaryGroups
if (hasAtLeastOneUninitializedMainDictionary()) {
asyncReloadUninitializedMainDictionaries(context, locales, listener)
}
}
listener?.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary())
// Clean up old dictionaries.
existingDictsToCleanup.forEach { (locale, dictTypes) ->
val dictGroupToCleanup = findDictionaryGroupWithLocale(oldDictionaryGroups, locale) ?: return@forEach
for (dictType in dictTypes) {
dictGroupToCleanup.closeDict(dictType)
}
}
mValidSpellingWordWriteCache?.evictAll()
mValidSpellingWordReadCache?.evictAll()
}
/** creates dictionaryGroups for [newLocales] with given [newSubDictTypes], trying to re-use existing dictionaries.
* returns the new dictionaryGroups and unused dictionary types by locale */
private fun getNewDictGroupsAndDictsToCleanup(
newLocales: Collection<Locale>,
newSubDictTypes: Collection<String>,
forceReload: Boolean,
dictNamePrefix: String,
context: Context
): Pair<List<DictionaryGroup>, Map<Locale, List<String>>> {
// Gather all dictionaries by locale. We may remove some from the list later.
val existingDictsToCleanup = HashMap<Locale, MutableList<String>>()
for (dictGroup in dictionaryGroups) {
existingDictsToCleanup[dictGroup.locale] = DictionaryFacilitator.ALL_DICTIONARY_TYPES
.filterTo(mutableListOf()) { dictGroup.hasDict(it) }
}
// create new dictionary groups and remove dictionaries to re-use from existingDictsToCleanup
val newDictionaryGroups = mutableListOf<DictionaryGroup>()
for (locale in newLocales) {
// get existing dictionary group for new locale
val oldDictGroupForLocale = findDictionaryGroupWithLocale(dictionaryGroups, locale)
val dictTypesToCleanupForLocale = existingDictsToCleanup[locale]
// create new or re-use already loaded main dict
val mainDict: Dictionary?
if (forceReload || oldDictGroupForLocale == null
|| !oldDictGroupForLocale.hasDict(Dictionary.TYPE_MAIN)
) {
mainDict = null // null main dicts will be loaded later in asyncReloadUninitializedMainDictionaries
} else {
mainDict = oldDictGroupForLocale.getDict(Dictionary.TYPE_MAIN)
dictTypesToCleanupForLocale?.remove(Dictionary.TYPE_MAIN)
}
// create new or re-use already loaded sub-dicts
val subDicts: MutableMap<String, ExpandableBinaryDictionary> = HashMap()
for (subDictType in newSubDictTypes) {
val subDict: ExpandableBinaryDictionary
if (forceReload || oldDictGroupForLocale == null
|| !oldDictGroupForLocale.hasDict(subDictType)
) {
// Create a new dictionary.
subDict = createSubDict(subDictType, context, locale, null, dictNamePrefix) ?: continue
} else {
// Reuse the existing dictionary.
subDict = oldDictGroupForLocale.getSubDict(subDictType) ?: continue
dictTypesToCleanupForLocale?.remove(subDictType)
}
subDicts[subDictType] = subDict
}
val newDictGroup = DictionaryGroup(locale, mainDict, subDicts, context)
newDictionaryGroups.add(newDictGroup)
}
return newDictionaryGroups to existingDictsToCleanup
}
private fun asyncReloadUninitializedMainDictionaries(
context: Context, locales: Collection<Locale>, listener: DictionaryInitializationListener?
) {
val latchForWaitingLoadingMainDictionary = CountDownLatch(1)
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary
scope.launch {
val dictGroupsWithNewMainDict = locales.mapNotNull {
val dictionaryGroup = findDictionaryGroupWithLocale(dictionaryGroups, it)
if (dictionaryGroup == null) {
Log.w(TAG, "Expected a dictionary group for $it but none found")
return@mapNotNull null // This should never happen
}
if (dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.isInitialized == true) null
else dictionaryGroup to DictionaryFactory.createMainDictionaryCollection(context, it)
}
synchronized(this) {
dictGroupsWithNewMainDict.forEach { (dictGroup, mainDict) ->
dictGroup.setMainDict(mainDict)
}
}
listener?.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary())
latchForWaitingLoadingMainDictionary.countDown()
}
}
override fun closeDictionaries() {
val dictionaryGroupsToClose: List<DictionaryGroup>
synchronized(this) {
dictionaryGroupsToClose = dictionaryGroups
dictionaryGroups = listOf(DictionaryGroup())
}
for (dictionaryGroup in dictionaryGroupsToClose) {
for (dictType in DictionaryFacilitator.ALL_DICTIONARY_TYPES) {
dictionaryGroup.closeDict(dictType)
}
}
}
// The main dictionaries are loaded asynchronously. Don't cache the return value of these methods.
override fun hasAtLeastOneInitializedMainDictionary(): Boolean =
dictionaryGroups.any { it.getDict(Dictionary.TYPE_MAIN)?.isInitialized == true }
override fun hasAtLeastOneUninitializedMainDictionary(): Boolean =
dictionaryGroups.any { it.getDict(Dictionary.TYPE_MAIN)?.isInitialized != true }
@Throws(InterruptedException::class)
override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit) {
mLatchForWaitingLoadingMainDictionaries.await(timeout, unit)
}
// -------------- actual dictionary stuff like getting suggestions ------------
override fun addToUserHistory(
suggestion: String, wasAutoCapitalized: Boolean, ngramContext: NgramContext,
timeStampInSeconds: Long, blockPotentiallyOffensive: Boolean
) {
// Update the spelling cache before learning. Words that are not yet added to user history
// and appear in no other language model are not considered valid.
putWordIntoValidSpellingWordCache("addToUserHistory", suggestion)
val words = suggestion.splitOnWhitespace().dropLastWhile { it.isEmpty() }
// increase / decrease confidence
if (words.size == 1) // ignore if more than a single word, which only happens with (badly working) spaceAwareGesture
adjustConfidences(suggestion, wasAutoCapitalized)
// Add word to user dictionary if it is in no other dictionary except user history dictionary (i.e. typed again).
val sv = Settings.getValues()
if (sv.mAddToPersonalDictionary // require the opt-in
&& sv.mAutoCorrectEnabled == sv.mAutoCorrectionEnabledPerUserSettings // don't add if user wants autocorrect but input field does not, see https://github.com/Helium314/HeliBoard/issues/427#issuecomment-1905438000
&& dictionaryGroups[0].hasDict(Dictionary.TYPE_USER_HISTORY) // require personalized suggestions
&& !wasAutoCapitalized // we can't be 100% sure about what the user intended to type, so better don't add it
&& words.size == 1 // only single words
) {
addToPersonalDictionaryIfInvalidButInHistory(suggestion)
}
var ngramContextForCurrentWord = ngramContext
val preferredGroup = currentlyPreferredDictionaryGroup
for (i in words.indices) {
val currentWord = words[i]
val wasCurrentWordAutoCapitalized = (i == 0) && wasAutoCapitalized
// add to history for preferred dictionary group, to avoid mixing languages in history
addWordToUserHistory(
preferredGroup, ngramContextForCurrentWord, currentWord,
wasCurrentWordAutoCapitalized, timeStampInSeconds.toInt(), blockPotentiallyOffensive
)
ngramContextForCurrentWord = ngramContextForCurrentWord.getNextNgramContext(WordInfo(currentWord))
// remove manually entered blacklisted words from blacklist for likely matching languages
dictionaryGroups.filter { it.confidence == preferredGroup.confidence }.forEach {
it.removeFromBlacklist(currentWord)
}
}
}
private fun addWordToUserHistory(
dictionaryGroup: DictionaryGroup, ngramContext: NgramContext, word: String, wasAutoCapitalized: Boolean,
timeStampInSeconds: Int, blockPotentiallyOffensive: Boolean
) {
val userHistoryDictionary = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
val mainFreq = dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.getFrequency(word) ?: Dictionary.NOT_A_PROBABILITY
if (mainFreq == 0 && blockPotentiallyOffensive)
return
if (tryChangingWords)
tryChangingWords = ngramContext.changeWordIfAfterBeginningOfSentence(changeFrom, changeTo)
val wordToUse: String
// Check for isBeginningOfSentenceContext too, because not all text fields auto-capitalize in this case.
// Even if the user capitalizes manually, they most likely don't want the capitalized form suggested.
if (wasAutoCapitalized || ngramContext.isBeginningOfSentenceContext) {
val decapitalizedWord = word.decapitalize(dictionaryGroup.locale) // try undoing auto-capitalization
if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup)
&& !isValidWord(decapitalizedWord, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup)
) {
// If the word was auto-capitalized and exists only as a capitalized word in the
// dictionary, then we must not downcase it before registering it. For example,
// the name of the contacts in start-of-sentence position would come here with the
// wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version
// of that contact's name which would end up popping in suggestions.
wordToUse = word
} else {
// If however the word is not in the dictionary, or exists as a de-capitalized word
// only, then we consider that was a lower-case word that had been auto-capitalized.
wordToUse = decapitalizedWord
tryChangingWords = true
changeFrom = word
changeTo = wordToUse
}
} else {
// HACK: We'd like to avoid adding the capitalized form of common words to the User
// History dictionary in order to avoid suggesting them until the dictionary
// consolidation is done.
// TODO: Remove this hack when ready.
val lowerCasedWord = word.lowercase(dictionaryGroup.locale)
val lowerCaseFreqInMainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.getFrequency(lowerCasedWord)
?: Dictionary.NOT_A_PROBABILITY
wordToUse = if (mainFreq < lowerCaseFreqInMainDict
&& lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT
) {
// Use lower cased word as the word can be a distracter of the popular word.
lowerCasedWord
} else {
word
}
}
// We demote unrecognized words (frequency <= 0) by specifying them as "invalid".
// We don't add words with 0-frequency (assuming they would be profanity etc.).
val isValid = mainFreq > 0
UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, wordToUse, isValid, timeStampInSeconds)
}
private fun addToPersonalDictionaryIfInvalidButInHistory(word: String) {
val dictionaryGroup = clearlyPreferredDictionaryGroup ?: return
val userDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER) ?: return
val userHistoryDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup))
return // valid word, no reason to auto-add it to personal dict
if (userDict.isInDictionary(word))
return // should never happen, but better be safe
// User history always reports words as invalid, so we check the frequency instead.
// Testing shows that after 2 times adding, the frequency is 111, and then rises slowly with usage (values vary slightly).
// 120 is after 3 uses of the word, so we simply require more than that. todo: Could be made configurable.
// Words added to dictionaries (user and history) seem to be found only after some delay.
// This is not too bad, but it delays adding in case a user wants to fill a dictionary using this functionality
if (userHistoryDict.getFrequency(word) > 120) {
scope.launch {
UserDictionary.Words.addWord(userDict.mContext, word, 250, null, dictionaryGroup.locale)
}
}
}
private fun putWordIntoValidSpellingWordCache(caller: String, originalWord: String) {
if (mValidSpellingWordWriteCache == null)
return
val lowerCaseWord = originalWord.lowercase(currentLocale)
val lowerCaseValid = isValidSpellingWord(lowerCaseWord)
mValidSpellingWordWriteCache?.put(lowerCaseWord, lowerCaseValid)
val capitalWord = StringUtils.capitalizeFirstAndDowncaseRest(originalWord, currentLocale)
val capitalValid = if (lowerCaseValid) {
true // The lower case form of the word is valid, so the upper case must be valid.
} else {
isValidSpellingWord(capitalWord)
}
mValidSpellingWordWriteCache?.put(capitalWord, capitalValid)
}
override fun adjustConfidences(word: String, wasAutoCapitalized: Boolean) {
if (dictionaryGroups.size == 1 || word.contains(Constants.WORD_SEPARATOR))
return
// if suggestion was auto-capitalized, check against both the suggestion and the de-capitalized suggestion
val decapitalizedSuggestion = if (wasAutoCapitalized) word.decapitalize(currentLocale) else word
dictionaryGroups.forEach {
if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it)) {
it.increaseConfidence()
return@forEach
}
// also increase confidence if suggestion was auto-capitalized and the lowercase variant it valid
if (wasAutoCapitalized && isValidWord(decapitalizedSuggestion, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it))
it.increaseConfidence()
else it.decreaseConfidence()
}
}
/** the dictionaryGroup with most confidence, first group when tied */
private val currentlyPreferredDictionaryGroup: DictionaryGroup get() = dictionaryGroups.maxBy { it.confidence }
/** the only dictionary group, or the dictionaryGroup confidence >= DictionaryGroup.MAX_CONFIDENCE if all others have 0 */
private val clearlyPreferredDictionaryGroup: DictionaryGroup? get() {
if (dictionaryGroups.size == 1) return dictionaryGroups.first() // confidence not used if we only have a single group
val preferred = currentlyPreferredDictionaryGroup
if (preferred.confidence < DictionaryGroup.MAX_CONFIDENCE) return null
if (dictionaryGroups.any { it.confidence > 0 && it !== preferred })
return null
return preferred
}
override fun unlearnFromUserHistory(word: String, ngramContext: NgramContext, timeStampInSeconds: Long, eventType: Int) {
// TODO: Decide whether or not to remove the word on EVENT_BACKSPACE.
if (eventType != Constants.EVENT_BACKSPACE) {
currentlyPreferredDictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY)?.removeUnigramEntryDynamically(word)
}
// Update the spelling cache after unlearning. Words that are removed from user history
// and appear in no other language model are not considered valid.
putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.lowercase(Locale.getDefault()))
}
// TODO: Revise the way to fusion suggestion results.
override fun getSuggestionResults(
composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int, inputStyle: Int
): SuggestionResults {
val proximityInfoHandle = keyboard.proximityInfo.nativeProximityInfo
val weightOfLangModelVsSpatialModel = floatArrayOf(Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL)
val waitForOtherDicts = if (dictionaryGroups.size == 1) null else CountDownLatch(dictionaryGroups.size - 1)
val suggestionsArray = Array<List<SuggestedWordInfo>?>(dictionaryGroups.size) { null }
for (i in 1..dictionaryGroups.lastIndex) {
scope.launch {
suggestionsArray[i] = getSuggestions(composedData, ngramContext, settingsValuesForSuggestion, sessionId,
proximityInfoHandle, weightOfLangModelVsSpatialModel, dictionaryGroups[i])
waitForOtherDicts?.countDown()
}
}
suggestionsArray[0] = getSuggestions(composedData, ngramContext, settingsValuesForSuggestion, sessionId,
proximityInfoHandle, weightOfLangModelVsSpatialModel, dictionaryGroups[0])
val suggestionResults = SuggestionResults(
SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext, false
)
waitForOtherDicts?.await()
suggestionsArray.forEach {
if (it == null) return@forEach
suggestionResults.addAll(it)
suggestionResults.mRawSuggestions?.addAll(it)
}
return suggestionResults
}
private fun getSuggestions(
composedData: ComposedData, ngramContext: NgramContext,
settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int,
proximityInfoHandle: Long, weightOfLangModelVsSpatialModel: FloatArray, dictGroup: DictionaryGroup
): List<SuggestedWordInfo> {
val suggestions = ArrayList<SuggestedWordInfo>()
val weightForLocale = dictGroup.getWeightForLocale(dictionaryGroups, composedData.mIsBatchMode)
for (dictType in DictionaryFacilitator.ALL_DICTIONARY_TYPES) {
val dictionary = dictGroup.getDict(dictType) ?: continue
val dictionarySuggestions = dictionary.getSuggestions(composedData, ngramContext, proximityInfoHandle,
settingsValuesForSuggestion, sessionId, weightForLocale, weightOfLangModelVsSpatialModel
) ?: continue
// For some reason "garbage" words are produced when glide typing. For user history
// and main dictionaries we can filter them out by checking whether the dictionary
// actually contains the word. But personal and addon dictionaries may contain shortcuts,
// which do not pass an isInDictionary check (e.g. emojis).
// (if the main dict contains shortcuts to non-words, this will break!)
val checkForGarbage = composedData.mIsBatchMode && (dictType == Dictionary.TYPE_USER_HISTORY || dictType == Dictionary.TYPE_MAIN)
for (info in dictionarySuggestions) {
val word = info.word
if (isBlacklisted(word) || SupportedEmojis.isUnsupported(word)) // don't add blacklisted words and unsupported emojis
continue
if (checkForGarbage
// consider the user might use custom main dictionary containing shortcuts
// assume this is unlikely to happen, and take care about common shortcuts that are not actual words (emoji, symbols)
&& word.length > 2 // should exclude most symbol shortcuts
&& info.mSourceDict.mDictType == dictType // dictType is always main, but info.mSourceDict.mDictType contains the actual dict (main dict is a dictionary group)
&& !StringUtils.mightBeEmoji(word) // simplified check for performance reasons
&& !dictionary.isInDictionary(word)
)
continue
if (word.length == 1 && info.mSourceDict.mDictType == "emoji" && !StringUtils.mightBeEmoji(word[0].code))
continue
suggestions.add(info)
}
}
return suggestions
}
// Spell checker is using this, and has its own instance of DictionaryFacilitatorImpl,
// meaning that it always has default mConfidence. So we cannot choose to only check preferred
// locale, and instead simply return true if word is in any of the available dictionaries
override fun isValidSpellingWord(word: String): Boolean {
mValidSpellingWordReadCache?.get(word)?.let { return it }
val result = dictionaryGroups.any { isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it) }
mValidSpellingWordReadCache?.put(word, result)
return result
}
// this is unused, so leave it for now (redirecting to isValidWord seems to defeat the purpose...)
override fun isValidSuggestionWord(word: String): Boolean {
return isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroups[0])
}
// todo: move into dictionaryGroup?
private fun isValidWord(word: String, dictionariesToCheck: Array<String>, dictionaryGroup: DictionaryGroup): Boolean {
if (word.isEmpty() || dictionaryGroup.isBlacklisted(word)) return false
return dictionariesToCheck.any { dictionaryGroup.getDict(it)?.isValidWord(word) == true }
}
private fun isBlacklisted(word: String): Boolean = dictionaryGroups.any { it.isBlacklisted(word) }
override fun removeWord(word: String) {
for (dictionaryGroup in dictionaryGroups) {
dictionaryGroup.removeWord(word)
}
}
override fun clearUserHistoryDictionary(context: Context) {
for (dictionaryGroup in dictionaryGroups) {
dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY)?.clear()
}
}
override fun localesAndConfidences(): String? {
if (dictionaryGroups.size < 2) return null
return dictionaryGroups.joinToString(", ") { "${it.locale} ${it.confidence}" }
}
override fun dumpDictionaryForDebug(dictName: String) {
val dictToDump = dictionaryGroups[0].getSubDict(dictName)
if (dictToDump == null) {
Log.e(TAG, ("Cannot dump $dictName. The dictionary is not being used for suggestion or cannot be dumped."))
return
}
dictToDump.dumpAllWordsForDebug()
}
override fun getDictionaryStats(context: Context): List<DictionaryStats> =
DictionaryFacilitator.DYNAMIC_DICTIONARY_TYPES.flatMap { dictType ->
dictionaryGroups.mapNotNull { it.getSubDict(dictType)?.dictionaryStats }
}
override fun dump(context: Context) = getDictionaryStats(context).joinToString("\n")
companion object {
private val TAG = DictionaryFacilitatorImpl::class.java.simpleName
// HACK: This threshold is being used when adding a capitalized entry in the User History dictionary.
private const val CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140
private fun createSubDict(
dictType: String, context: Context, locale: Locale, dictFile: File?, dictNamePrefix: String
): ExpandableBinaryDictionary? {
try {
return when (dictType) {
Dictionary.TYPE_USER_HISTORY -> UserHistoryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
Dictionary.TYPE_USER -> UserBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
Dictionary.TYPE_CONTACTS -> ContactsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
Dictionary.TYPE_APPS -> AppsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
else -> throw IllegalArgumentException("unknown dictionary type $dictType")
}
} catch (e: SecurityException) {
Log.e(TAG, "Cannot create dictionary: $dictType", e)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Cannot create dictionary: $dictType", e)
}
return null
}
private fun findDictionaryGroupWithLocale(dictGroups: List<DictionaryGroup>?, locale: Locale): DictionaryGroup? {
return dictGroups?.firstOrNull { it.locale == locale }
}
private fun getUsedLocales(mainLocale: Locale, context: Context): Collection<Locale> {
val locales = hashSetOf(mainLocale)
// adding secondary locales is a bit tricky since they depend on the subtype
// but usually this is called with the selected subtype locale
val selectedSubtype = SubtypeSettings.getSelectedSubtype(context.prefs())
if (selectedSubtype.locale() == mainLocale) {
locales.addAll(getSecondaryLocales(selectedSubtype.extraValue))
} else {
// probably we're called from the spell checker when using a different app as keyboard
// so best bet is adding all secondary locales for matching main locale
SubtypeSettings.getEnabledSubtypes(false).forEach {
if (it.locale() == mainLocale)
locales.addAll(getSecondaryLocales(it.extraValue))
}
}
return locales
}
}
}
/** A group of dictionaries that work together for a single language. */
private class DictionaryGroup(
val locale: Locale = Locale(""),
private var mainDict: Dictionary? = null,
subDicts: Map<String, ExpandableBinaryDictionary> = emptyMap(),
context: Context? = null
) {
private val subDicts: ConcurrentHashMap<String, ExpandableBinaryDictionary> = ConcurrentHashMap(subDicts)
/** Removes a word from all dictionaries in this group. If the word is in a read-only dictionary, it is blacklisted. */
fun removeWord(word: String) {
// remove from user history
getSubDict(Dictionary.TYPE_USER_HISTORY)?.removeUnigramEntryDynamically(word)
// and from personal dictionary
getSubDict(Dictionary.TYPE_USER)?.removeUnigramEntryDynamically(word)
val contactsDict = getSubDict(Dictionary.TYPE_CONTACTS)
if (contactsDict != null && contactsDict.isInDictionary(word)) {
contactsDict.removeUnigramEntryDynamically(word) // will be gone until next reload of dict
addToBlacklist(word)
return
}
val appsDict = getSubDict(Dictionary.TYPE_APPS)
if (appsDict != null && appsDict.isInDictionary(word)) {
appsDict.removeUnigramEntryDynamically(word) // will be gone until next reload of dict
addToBlacklist(word)
return
}
val mainDict = mainDict ?: return
if (mainDict.isValidWord(word)) {
addToBlacklist(word)
return
}
val lowercase = word.lowercase(locale)
if (getDict(Dictionary.TYPE_MAIN)!!.isValidWord(lowercase)) {
addToBlacklist(lowercase)
}
}
// --------------- Confidence for multilingual typing -------------------
// Confidence that the most probable language is actually the language the user is
// typing in. For now, this is simply the number of times a word from this language
// has been committed in a row, with an exception when typing a single word not contained
// in this language.
var confidence = 1
// allow to go above max confidence, for better determination of currently preferred language
// when decreasing confidence or getting weight factor, limit to maximum
fun increaseConfidence() {
confidence += 1
}
// If confidence is above max, drop to max confidence. This does not change weights and
// allows conveniently typing single words from the other language without affecting suggestions
fun decreaseConfidence() {
if (confidence > MAX_CONFIDENCE) confidence = MAX_CONFIDENCE
else if (confidence > 0) {
confidence -= 1
}
}
fun getWeightForLocale(groups: List<DictionaryGroup>, isGesturing: Boolean) =
getWeightForLocale(groups, if (isGesturing) 0.05f else 0.15f)
// might need some more tuning
fun getWeightForLocale(groups: List<DictionaryGroup>, step: Float): Float {
if (groups.size == 1) return 1f
if (confidence < 2) return 1f - step * (MAX_CONFIDENCE - confidence)
for (group in groups) {
if (group !== this && group.confidence >= confidence) return 1f - step / 2f
}
return 1f
}
// --------------- Blacklist -------------------
private val scope = CoroutineScope(Dispatchers.IO)
// words cannot be (permanently) removed from some dictionaries, so we use a blacklist for "removing" words
private val blacklistFile = if (context?.filesDir == null) null
else {
val file = File(context.filesDir.absolutePath + File.separator + "blacklists" + File.separator + locale.toLanguageTag() + ".txt")
if (file.isDirectory) file.delete() // this apparently was an issue in some versions
if (file.mkdirs()) file
else null
}
private val blacklist = hashSetOf<String>().apply {
if (blacklistFile?.exists() != true) return@apply
scope.launch {
synchronized(this) {
try {
addAll(blacklistFile.readLines())
} catch (e: IOException) {
Log.e(TAG, "Exception while trying to read blacklist from ${blacklistFile.name}", e)
}
}
}
}
fun isBlacklisted(word: String) = blacklist.contains(word)
fun addToBlacklist(word: String) {
if (!blacklist.add(word) || blacklistFile == null) return
scope.launch {
synchronized(this) {
try {
if (blacklistFile.isDirectory) blacklistFile.delete()
blacklistFile.appendText("$word\n")
} catch (e: IOException) {
Log.e(TAG, "Exception while trying to add word \"$word\" to blacklist ${blacklistFile.name}", e)
}
}
}
}
fun removeFromBlacklist(word: String) {
if (!blacklist.remove(word) || blacklistFile == null) return
scope.launch {
synchronized(this) {
try {
val newLines = blacklistFile.readLines().filterNot { it == word }
blacklistFile.writeText(newLines.joinToString("\n"))
} catch (e: IOException) {
Log.e(TAG, "Exception while trying to remove word \"$word\" to blacklist ${blacklistFile.name}", e)
}
}
}
}
// --------------- Dictionary handling -------------------
fun setMainDict(newMainDict: Dictionary?) {
// Close old dictionary if exists. Main dictionary can be assigned multiple times.
val oldDict = mainDict
mainDict = newMainDict
if (oldDict != null && newMainDict !== oldDict)
oldDict.close()
}
fun getDict(dictType: String): Dictionary? {
if (dictType == Dictionary.TYPE_MAIN) {
return mainDict
}
return getSubDict(dictType)
}
fun getSubDict(dictType: String): ExpandableBinaryDictionary? {
return subDicts[dictType]
}
fun hasDict(dictType: String): Boolean {
if (dictType == Dictionary.TYPE_MAIN) {
return mainDict != null
}
return subDicts.containsKey(dictType)
}
fun closeDict(dictType: String) {
val dict = if (Dictionary.TYPE_MAIN == dictType) {
mainDict
} else {
subDicts.remove(dictType)
}
dict?.close()
}
companion object {
private val TAG = DictionaryGroup::class.java.simpleName
const val MAX_CONFIDENCE = 2
}
}

View file

@ -26,6 +26,7 @@ public class DictionaryFacilitatorLruCache {
private final Object mLock = new Object();
private final DictionaryFacilitator mDictionaryFacilitator;
private boolean mUseContactsDictionary;
private boolean mUseAppsDictionary;
private Locale mLocale;
public DictionaryFacilitatorLruCache(final Context context, final String dictionaryNamePrefix) {
@ -58,10 +59,8 @@ public class DictionaryFacilitatorLruCache {
// Nothing to do if the locale is null. This would be the case before any get() calls.
if (mLocale != null) {
// Note: Given that personalized dictionaries are not used here; we can pass null account.
mDictionaryFacilitator.resetDictionaries(mContext, mLocale,
mUseContactsDictionary, false /* usePersonalizedDicts */,
false /* forceReloadMainDictionary */, null /* account */,
mDictionaryNamePrefix, null /* listener */);
mDictionaryFacilitator.resetDictionaries(mContext, mLocale, mUseContactsDictionary,
mUseAppsDictionary, false, false, mDictionaryNamePrefix, null);
}
}
@ -77,6 +76,18 @@ public class DictionaryFacilitatorLruCache {
}
}
public void setUseAppsDictionary(final boolean useAppsDictionary) {
synchronized (mLock) {
if (mUseAppsDictionary == useAppsDictionary) {
// The value has not been changed.
return;
}
mUseAppsDictionary = useAppsDictionary;
resetDictionariesForLocaleLocked();
waitForLoadingMainDictionary(mDictionaryFacilitator);
}
}
public DictionaryFacilitator get(final Locale locale) {
synchronized (mLock) {
if (!mDictionaryFacilitator.isForLocale(locale)) {

View file

@ -6,88 +6,87 @@
package helium314.keyboard.latin
import android.content.Context
import helium314.keyboard.latin.common.FileUtils
import helium314.keyboard.latin.common.LocaleUtils
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.Log
import java.io.File
import java.util.LinkedList
import java.util.Locale
/**
* Initializes a main dictionary collection from a dictionary pack, with explicit flags.
*
*
* This searches for a content provider providing a dictionary pack for the specified
* locale. If none is found, it falls back to the built-in dictionary - if any.
* @param context application context for reading resources
* @param locale the locale for which to create the dictionary
* @return an initialized instance of DictionaryCollection
*/
fun createMainDictionary(context: Context, locale: Locale): DictionaryCollection {
val cacheDir = DictionaryInfoUtils.getAndCreateCacheDirectoryForLocale(locale, context)
val dictList = LinkedList<Dictionary>()
// get cached dict files
val (userDicts, extractedDicts) = DictionaryInfoUtils.getCachedDictsForLocale(locale, context)
.partition { it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) }
// add user dicts to list
userDicts.forEach { checkAndAddDictionaryToListIfNotExisting(it, dictList, locale) }
// add extracted dicts to list (after userDicts, to skip extracted dicts of same type)
extractedDicts.forEach { checkAndAddDictionaryToListIfNotExisting(it, dictList, locale) }
if (dictList.any { it.mDictType == Dictionary.TYPE_MAIN })
return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList)
// no main dict found -> check assets
val assetsDicts = DictionaryInfoUtils.getAssetsDictionaryList(context)
// file name is <type>_<language tag>.dict
val dictsByType = assetsDicts?.groupBy { it.substringBefore("_") }
// for each type find the best match
dictsByType?.forEach { (dictType, dicts) ->
val bestMatch = LocaleUtils.getBestMatch(locale, dicts) { it.substringAfter("_")
.substringBefore(".").constructLocale() } ?: return@forEach
// extract dict and add extracted file
val targetFile = File(cacheDir, "$dictType.dict")
FileUtils.copyStreamToNewFile(
context.assets.open(DictionaryInfoUtils.ASSETS_DICTIONARY_FOLDER + File.separator + bestMatch),
targetFile
)
checkAndAddDictionaryToListIfNotExisting(targetFile, dictList, locale)
}
// If the list is empty, that means we should not use any dictionary (for example, the user
// explicitly disabled the main dictionary), so the following is okay. dictList is never
// null, but if for some reason it is, DictionaryCollection handles it gracefully.
return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList)
}
/**
* add dictionary created from [file] to [dicts]
* if [file] cannot be loaded it is deleted
* if the dictionary type already exists in [dicts], the [file] is skipped
*/
private fun checkAndAddDictionaryToListIfNotExisting(file: File, dicts: MutableList<Dictionary>, locale: Locale) {
if (!file.isFile) return
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file) ?: return killDictionary(file)
val dictType = header.mIdString.split(":").first()
if (dicts.any { it.mDictType == dictType }) return
val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
file.absolutePath, 0, file.length(), false, locale, dictType
)
if (readOnlyBinaryDictionary.isValidDictionary) {
if (locale.language == "ko") {
// Use KoreanDictionary for Korean locale
dicts.add(KoreanDictionary(readOnlyBinaryDictionary))
} else {
dicts.add(readOnlyBinaryDictionary)
object DictionaryFactory {
/**
* Initializes a main dictionary collection for a locale.
* Uses all dictionaries in cache folder for locale, and adds built-in
* dictionaries of matching locales if type is not already in cache folder.
*
* @return an initialized instance of DictionaryCollection
*/
// todo:
// expose the weight so users can adjust dictionary "importance" (useful for addons like emoji dict)
// allow users to block certain dictionaries (not sure how this should work exactly)
fun createMainDictionaryCollection(context: Context, locale: Locale): DictionaryCollection {
val dictList = LinkedList<Dictionary>()
val (extracted, nonExtracted) = getAvailableDictsForLocale(locale, context)
extracted.sortedBy { !it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) }.forEach {
// we sort to have user dicts first, so they have priority over internal dicts of the same type
checkAndAddDictionaryToListNewType(it, dictList, locale)
}
} else {
readOnlyBinaryDictionary.close()
killDictionary(file)
nonExtracted.forEach { filename ->
val type = filename.substringBefore("_")
if (dictList.any { it.mDictType == type }) return@forEach
val extractedFile = DictionaryInfoUtils.extractAssetsDictionary(filename, locale, context) ?: return@forEach
checkAndAddDictionaryToListNewType(extractedFile, dictList, locale)
}
return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList, FloatArray(dictList.size) { 1f })
}
fun getAvailableDictsForLocale(locale: Locale, context: Context): Pair<Array<out File>, List<String>> {
val cachedDicts = DictionaryInfoUtils.getCachedDictsForLocale(locale, context)
val nonExtractedDicts = mutableListOf<String>()
DictionaryInfoUtils.getAssetsDictionaryList(context)
// file name is <type>_<language tag>.dict
?.groupBy { it.substringBefore("_") }
?.forEach { (dictType, dicts) ->
if (cachedDicts.any { it.name == "$dictType.dict" })
return@forEach // dictionary is already extracted (can't be old because of cleanup on upgrade)
val bestMatch = LocaleUtils.getBestMatch(locale, dicts) {
DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(it)
} ?: return@forEach
nonExtractedDicts.add(bestMatch)
}
return cachedDicts to nonExtractedDicts
}
/**
* add dictionary created from [file] to [dicts]
* if [file] cannot be loaded it is deleted
* if the dictionary type already exists in [dicts], the [file] is skipped
*/
private fun checkAndAddDictionaryToListNewType(file: File, dicts: MutableList<Dictionary>, locale: Locale) {
if (!file.isFile) return
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file) ?: return killDictionary(file)
val dictType = header.mIdString.split(":").first()
if (dicts.any { it.mDictType == dictType }) return
val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
file.absolutePath, 0, file.length(), false, locale, dictType
)
if (readOnlyBinaryDictionary.isValidDictionary) {
if (locale.language == "ko") {
// Use KoreanDictionary for Korean locale
dicts.add(KoreanDictionary(readOnlyBinaryDictionary))
} else {
dicts.add(readOnlyBinaryDictionary)
}
} else {
readOnlyBinaryDictionary.close()
killDictionary(file)
}
}
private fun killDictionary(file: File) {
Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
file.delete()
}
}
private fun killDictionary(file: File) {
Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
file.delete()
}

View file

@ -21,6 +21,8 @@ import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.suggestions.PopupSuggestionsView;
import helium314.keyboard.latin.suggestions.SuggestionStripView;
import kotlin.Unit;
public final class InputView extends FrameLayout {
private final Rect mInputViewRect = new Rect();
@ -43,10 +45,7 @@ public final class InputView extends FrameLayout {
mMainKeyboardView, suggestionStripView);
mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler(
mMainKeyboardView, suggestionStripView);
ViewKt.doOnNextLayout(this, v -> {
Settings.getValues().mColors.setBackground(findViewById(R.id.main_keyboard_frame), ColorType.MAIN_BACKGROUND);
return null;
});
ViewKt.doOnNextLayout(this, this::onNextLayout);
}
public void setKeyboardTopPadding(final int keyboardTopPadding) {
@ -104,6 +103,14 @@ public final class InputView extends FrameLayout {
return mActiveForwarder.onTouchEvent(x, y, me);
}
private Unit onNextLayout(View v) {
Settings.getValues().mColors.setBackground(findViewById(R.id.main_keyboard_frame), ColorType.MAIN_BACKGROUND);
// Work around inset application being unreliable
requestApplyInsets();
return null;
}
/**
* This class forwards series of {@link MotionEvent}s from <code>SenderView</code> to
* <code>ReceiverView</code>.

View file

@ -78,7 +78,7 @@ class KeyboardWrapperView @JvmOverloads constructor(
val changePercent = 2 * sign * (x - motionEvent.rawX) / context.resources.displayMetrics.density
if (abs(changePercent) < 1) return@setOnTouchListener true
x = motionEvent.rawX
val oldScale = Settings.readOneHandedModeScale(context.prefs(), Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT)
val oldScale = Settings.readOneHandedModeScale(context.prefs(), Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE)
val newScale = (oldScale + changePercent / 100f).coerceAtMost(2.5f).coerceAtLeast(0.5f)
if (newScale == oldScale) return@setOnTouchListener true
Settings.getInstance().writeOneHandedModeScale(newScale)

View file

@ -7,6 +7,7 @@
package helium314.keyboard.latin;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -100,7 +101,6 @@ import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
/**
@ -523,6 +523,11 @@ public class LatinIME extends InputMethodService implements
}
final class SubtypeState {
// When HintLocales causes a subtype override, we store
// the overridden subtype here in order to restore it when
// we switch to another input context that has no HintLocales.
private InputMethodSubtype mOverriddenByLocale;
private InputMethodSubtype mLastActiveSubtype;
private boolean mCurrentSubtypeHasBeenUsed = true; // starting with true avoids immediate switch
@ -530,6 +535,70 @@ public class LatinIME extends InputMethodService implements
mCurrentSubtypeHasBeenUsed = true;
}
// TextFields can provide locale/language hints that the IME should use via 'hintLocales'.
// If a matching subtype is found, we temporarily switch to that subtype until
// we return to a context that does not provide any hints, or until the user
// explicitly changes the language/subtype in use.
public InputMethodSubtype getSubtypeForLocales(final RichInputMethodManager richImm, final Iterable<Locale> locales) {
final InputMethodSubtype overriddenByLocale = mOverriddenByLocale;
if (locales == null) {
if (overriddenByLocale != null) {
// no locales provided, so switch back to
// whatever subtype was used last time.
mOverriddenByLocale = null;
return overriddenByLocale;
}
return null;
}
final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype();
final Locale currentSubtypeLocale = richImm.getCurrentSubtypeLocale();
final int minimumMatchLevel = 3; // LocaleUtils.LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
// Try finding a subtype matching the hint language.
for (final Locale hintLocale : locales) {
if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, currentSubtypeLocale) >= minimumMatchLevel
|| CollectionsKt.any(mSettings.getCurrent().mSecondaryLocales,
(secLocale) -> LocaleUtils.INSTANCE.getMatchLevel(hintLocale, secLocale) >= minimumMatchLevel)) {
// current locales are already a good match, and we want to avoid unnecessary layout switches.
return null;
}
final InputMethodSubtype subtypeForHintLocale = richImm.findSubtypeForHintLocale(hintLocale);
if (subtypeForHintLocale == null) {
continue;
}
if (subtypeForHintLocale.equals(currentSubtype)) {
// no need to switch, we already use the correct locale.
return null;
}
if (overriddenByLocale == null) {
// auto-switching based on hint locale, so store
// whatever subtype was in use so we can switch back
// to it later when there are no hint locales.
mOverriddenByLocale = currentSubtype;
}
return subtypeForHintLocale;
}
return null;
}
public void onSubtypeChanged(final InputMethodSubtype oldSubtype,
final InputMethodSubtype newSubtype) {
if (oldSubtype != mOverriddenByLocale) {
// Whenever the subtype is changed, clear tracking
// the subtype that is overridden by a HintLocale as
// we no longer have a subtype to automatically switch back to.
mOverriddenByLocale = null;
}
}
public void switchSubtype(final RichInputMethodManager richImm) {
final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype();
final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
@ -668,8 +737,8 @@ public class LatinIME extends InputMethodService implements
if (mDictionaryFacilitator.usesSameSettings(
locales,
mSettings.getCurrent().mUseContactsDictionary,
mSettings.getCurrent().mUsePersonalizedDicts,
mSettings.getCurrent().mAccount
mSettings.getCurrent().mUseAppsDictionary,
mSettings.getCurrent().mUsePersonalizedDicts
)) {
return;
}
@ -686,8 +755,8 @@ public class LatinIME extends InputMethodService implements
private void resetDictionaryFacilitator(@NonNull final Locale locale) {
final SettingsValues settingsValues = mSettings.getCurrent();
mDictionaryFacilitator.resetDictionaries(this, locale,
settingsValues.mUseContactsDictionary, settingsValues.mUsePersonalizedDicts,
false, settingsValues.mAccount, "", this);
settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
settingsValues.mUsePersonalizedDicts, false, "", this);
mInputLogic.mSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
}
@ -696,12 +765,9 @@ public class LatinIME extends InputMethodService implements
*/
/* package private */ void resetSuggestMainDict() {
final SettingsValues settingsValues = mSettings.getCurrent();
mDictionaryFacilitator.resetDictionaries(this /* context */,
mDictionaryFacilitator.getMainLocale(), settingsValues.mUseContactsDictionary,
settingsValues.mUsePersonalizedDicts,
true /* forceReloadMainDictionary */,
settingsValues.mAccount, "" /* dictNamePrefix */,
this /* DictionaryInitializationListener */);
mDictionaryFacilitator.resetDictionaries(this, mDictionaryFacilitator.getMainLocale(),
settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
settingsValues.mUsePersonalizedDicts, true, "", this);
}
// used for debug
@ -858,6 +924,8 @@ public class LatinIME extends InputMethodService implements
return;
}
InputMethodSubtype oldSubtype = mRichImm.getCurrentSubtype().getRawSubtype();
mSubtypeState.onSubtypeChanged(oldSubtype, subtype);
StatsUtils.onSubtypeChanged(oldSubtype, subtype);
mRichImm.onSubtypeChanged(subtype);
mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype),
@ -876,20 +944,10 @@ public class LatinIME extends InputMethodService implements
super.onStartInput(editorInfo, restarting);
final List<Locale> hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo);
if (hintLocales == null) {
return;
}
// Try switching to a subtype matching the hint language.
for (final Locale hintLocale : hintLocales) {
if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, mRichImm.getCurrentSubtypeLocale()) >= 3
|| CollectionsKt.any(mSettings.getCurrent().mSecondaryLocales, (secLocale) -> LocaleUtils.INSTANCE.getMatchLevel(hintLocale, secLocale) >= 3))
return; // current locales are already a good match, and we want to avoid unnecessary layout switches
final InputMethodSubtype newSubtype = mRichImm.findSubtypeForHintLocale(hintLocale);
if (newSubtype == null) continue;
if (newSubtype.equals(mRichImm.getCurrentSubtype().getRawSubtype()))
return; // no need to switch, we already use the correct locale
mHandler.postSwitchLanguage(newSubtype);
break;
final InputMethodSubtype subtypeForLocales = mSubtypeState.getSubtypeForLocales(mRichImm, hintLocales);
if (subtypeForLocales != null) {
// found a better subtype using hint locales that we should switch to.
mHandler.postSwitchLanguage(subtypeForLocales);
}
}
@ -1403,7 +1461,7 @@ public class LatinIME extends InputMethodService implements
// switch IME if wanted and possible
if (switchIme && !switchSubtype && switchInputMethod())
return;
final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1;
final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(true).size() > 1;
// switch subtype if wanted, do nothing if no other subtype is available
if (switchSubtype && !switchIme) {
if (hasMoreThanOneSubtype)
@ -1960,8 +2018,10 @@ public class LatinIME extends InputMethodService implements
public void onTrimMemory(int level) {
super.onTrimMemory(level);
switch (level) {
case TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_RUNNING_CRITICAL, TRIM_MEMORY_COMPLETE ->
KeyboardLayoutSet.onSystemLocaleChanged(); // clears caches, nothing else
case TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_RUNNING_CRITICAL, TRIM_MEMORY_COMPLETE -> {
KeyboardLayoutSet.onSystemLocaleChanged(); // clears caches, nothing else
mKeyboardSwitcher.trimMemory();
}
// deallocateMemory always called on hiding, and should not be called when showing
}
}

View file

@ -25,6 +25,7 @@ import helium314.keyboard.latin.utils.ScriptUtils;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
import helium314.keyboard.latin.utils.SubtypeSettings;
import helium314.keyboard.latin.utils.SubtypeUtilsKt;
import kotlin.collections.CollectionsKt;
import java.util.Collections;
import java.util.HashMap;
@ -91,9 +92,6 @@ public class RichInputMethodManager {
mContext = context;
mInputMethodInfoCache = new InputMethodInfoCache(mImm, context.getPackageName());
// Initialize subtype utils.
SubtypeLocaleUtils.init(context);
// Initialize the current input method subtype and the shortcut IME.
refreshSubtypeCaches();
}
@ -148,13 +146,15 @@ public class RichInputMethodManager {
if (mCachedThisImeInfo != null) {
return mCachedThisImeInfo;
}
for (final InputMethodInfo imi : mImm.getInputMethodList()) {
final var inputMethods = mImm.getInputMethodList();
for (final InputMethodInfo imi : inputMethods) {
if (imi.getPackageName().equals(mImePackageName)) {
mCachedThisImeInfo = imi;
return imi;
}
}
throw new RuntimeException("Input method id for " + mImePackageName + " not found.");
throw new RuntimeException("Input method id for " + mImePackageName + " not found, only found" +
CollectionsKt.map(inputMethods, InputMethodInfo::getPackageName));
}
public synchronized List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
@ -302,9 +302,8 @@ public class RichInputMethodManager {
final int count = myImi.getSubtypeCount();
for (int i = 0; i < count; i++) {
final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
final String layoutName = SubtypeLocaleUtils.getMainLayoutName(subtype);
if (locale.equals(SubtypeUtilsKt.locale(subtype))
&& keyboardLayoutSetName.equals(layoutName)) {
final String layoutName = SubtypeUtilsKt.mainLayoutNameOrQwerty(subtype);
if (locale.equals(SubtypeUtilsKt.locale(subtype)) && keyboardLayoutSetName.equals(layoutName)) {
return subtype;
}
}

View file

@ -10,10 +10,11 @@ import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.common.LocaleUtils.isRtlLanguage
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.locale
import java.util.Locale
@ -25,7 +26,7 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
val locale: Locale = rawSubtype.locale()
// The subtype is considered RTL if the language of the main subtype is RTL.
val isRtlSubtype: Boolean = isRtlLanguage(locale)
val isRtlSubtype: Boolean = ScriptUtils.isScriptRtl(locale.script())
fun getExtraValueOf(key: String): String? = rawSubtype.getExtraValueOf(key)
@ -40,21 +41,9 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
val isCustom: Boolean get() = LayoutUtilsCustom.isCustomLayout(mainLayoutName)
val fullDisplayName: String get() {
if (isNoLanguage) {
return SubtypeLocaleUtils.getMainLayoutDisplayName(rawSubtype)!!
}
return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(locale)
}
val fullDisplayName: String get() = SubtypeLocaleUtils.getSubtypeLocaleDisplayName(locale)
val middleDisplayName: String
// Get the RichInputMethodSubtype's middle display name in its locale.
get() {
if (isNoLanguage) {
return SubtypeLocaleUtils.getMainLayoutDisplayName(rawSubtype)!!
}
return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(locale)
}
val middleDisplayName: String get() = SubtypeLocaleUtils.getSubtypeLanguageDisplayName(locale)
override fun equals(other: Any?): Boolean {
if (other !is RichInputMethodSubtype) return false
@ -81,7 +70,7 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE)
private val DUMMY_NO_LANGUAGE_SUBTYPE = RichInputMethodSubtype(
InputMethodSubtypeBuilder()
.setSubtypeNameResId(R.string.subtype_no_language_qwerty)
.setSubtypeNameResId(R.string.subtype_no_language)
.setSubtypeIconResId(R.drawable.ic_ime_switcher)
.setSubtypeLocale(SubtypeLocaleUtils.NO_LANGUAGE)
.setSubtypeMode(Constants.Subtype.KEYBOARD_MODE)
@ -132,4 +121,4 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
return DUMMY_NO_LANGUAGE_SUBTYPE
}
}
}
}

View file

@ -0,0 +1,121 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin
import android.content.Context
import android.util.LruCache
import helium314.keyboard.keyboard.Keyboard
import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
import helium314.keyboard.latin.common.ComposedData
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
import helium314.keyboard.latin.utils.SuggestionResults
import java.util.Locale
import java.util.concurrent.TimeUnit
/** Simple DictionaryFacilitator for a single Dictionary. Has some optional special purpose functionality. */
class SingleDictionaryFacilitator(private val dict: Dictionary) : DictionaryFacilitator {
var suggestionLogger: SuggestionLogger? = null
// this will not work from spell checker if used together with a different keyboard app
fun getSuggestions(word: String): SuggestionResults {
val suggestionResults = getSuggestionResults(
ComposedData.createForWord(word),
NgramContext.getEmptyPrevWordsContext(0),
KeyboardSwitcher.getInstance().keyboard, // looks like actual keyboard doesn't matter (composed data doesn't contain coordinates)
SettingsValuesForSuggestion(false, false),
Suggest.SESSION_ID_TYPING, SuggestedWords.INPUT_STYLE_TYPING
)
return suggestionResults
}
override fun getSuggestionResults(
composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int, inputStyle: Int
): SuggestionResults {
val suggestionResults = SuggestionResults(
SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext,
false
)
suggestionResults.addAll(
dict.getSuggestions(composedData, ngramContext, keyboard.proximityInfo.nativeProximityInfo,
settingsValuesForSuggestion, sessionId, 1f,
floatArrayOf(Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL)
)
)
suggestionLogger?.onNewSuggestions(suggestionResults, composedData, ngramContext, keyboard, inputStyle)
return suggestionResults
}
// ------------ dummy functionality ----------------
override fun setValidSpellingWordReadCache(cache: LruCache<String, Boolean>) {}
override fun setValidSpellingWordWriteCache(cache: LruCache<String, Boolean>) {}
override fun isForLocale(locale: Locale?): Boolean = locale == dict.mLocale
override fun onStartInput() {}
override fun onFinishInput(context: Context) {
dict.onFinishInput()
}
override fun closeDictionaries() {
dict.close()
}
override fun isActive(): Boolean = true
override fun getMainLocale(): Locale = dict.mLocale
override fun getCurrentLocale(): Locale = mainLocale
override fun usesSameSettings(locales: List<Locale>, contacts: Boolean, apps: Boolean, personalization: Boolean): Boolean {
return locales.singleOrNull() == mainLocale
}
override fun resetDictionaries(context: Context, newLocale: Locale, useContactsDict: Boolean, useAppsDict: Boolean,
usePersonalizedDicts: Boolean, forceReloadMainDictionary: Boolean, dictNamePrefix: String, listener: DictionaryInitializationListener?
) { }
override fun hasAtLeastOneInitializedMainDictionary(): Boolean = dict.isInitialized
override fun hasAtLeastOneUninitializedMainDictionary(): Boolean = !dict.isInitialized
override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit) {
}
override fun addToUserHistory(
suggestion: String, wasAutoCapitalized: Boolean, ngramContext: NgramContext,
timeStampInSeconds: Long, blockPotentiallyOffensive: Boolean
) {}
override fun adjustConfidences(word: String, wasAutoCapitalized: Boolean) {}
override fun unlearnFromUserHistory(word: String, ngramContext: NgramContext, timeStampInSeconds: Long, eventType: Int) {}
override fun isValidSpellingWord(word: String): Boolean = dict.isValidWord(word)
override fun isValidSuggestionWord(word: String) = isValidSpellingWord(word)
override fun removeWord(word: String) {}
override fun clearUserHistoryDictionary(context: Context) {}
override fun localesAndConfidences(): String? = null
override fun dumpDictionaryForDebug(dictName: String) {}
override fun getDictionaryStats(context: Context): List<DictionaryStats> = emptyList()
override fun dump(context: Context) = getDictionaryStats(context).joinToString("\n")
companion object {
interface SuggestionLogger {
/** provides input data and suggestions returned by the library */
fun onNewSuggestions(suggestions: SuggestionResults, composedData: ComposedData,
ngramContext: NgramContext, keyboard: Keyboard, inputStyle: Int)
}
}
}

View file

@ -82,8 +82,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
}
public static UserBinaryDictionary getDictionary(
final Context context, final Locale locale, final File dictFile,
final String dictNamePrefix, @Nullable final String account) {
final Context context, final Locale locale, final File dictFile, final String dictNamePrefix) {
return new UserBinaryDictionary(context, locale, false, dictFile, dictNamePrefix + NAME);
}

View file

@ -218,6 +218,11 @@ public final class WordComposer {
// TODO: compute where that puts us inside the events
}
public void resetInvalidCursorPosition() {
if (mCursorPositionWithinWord > mCodePointSize)
mCursorPositionWithinWord = 0;
}
public boolean isCursorFrontOrMiddleOfComposingWord() {
if (DebugFlags.DEBUG_ENABLED && mCursorPositionWithinWord > mCodePointSize) {
throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
@ -474,6 +479,10 @@ public final class WordComposer {
return mIsBatchMode;
}
public void unsetBatchMode() {
mIsBatchMode = false;
}
public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
mRejectedBatchModeSuggestion = rejectedSuggestion;
}

View file

@ -274,11 +274,11 @@ class DynamicColors(context: Context, override val themeStyle: String, override
override fun get(color: ColorType): Int = when (color) {
TOOL_BAR_KEY_ENABLED_BACKGROUND, EMOJI_CATEGORY_SELECTED, ACTION_KEY_BACKGROUND,
CLIPBOARD_PIN, SHIFT_KEY_ICON -> accent
AUTOFILL_BACKGROUND_CHIP, GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW -> adjustedBackground
AUTOFILL_BACKGROUND_CHIP, GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW_BACKGROUND -> adjustedBackground
TOOL_BAR_EXPAND_KEY_BACKGROUND -> if (!isNight) accent else doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, SUGGESTION_AUTO_CORRECT, REMOVE_SUGGESTION_ICON,
KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
KEY_TEXT, SUGGESTION_AUTO_CORRECT, REMOVE_SUGGESTION_ICON, EMOJI_KEY_TEXT, KEY_PREVIEW_TEXT, POPUP_KEY_TEXT,
KEY_ICON, POPUP_KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
KEY_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
@ -327,7 +327,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
EMOJI_CATEGORY_SELECTED, CLIPBOARD_PIN, SHIFT_KEY_ICON -> accentColorFilter
REMOVE_SUGGESTION_ICON, EMOJI_CATEGORY, KEY_TEXT,
KEY_ICON, ONE_HANDED_MODE_BUTTON, TOOL_BAR_KEY, TOOL_BAR_EXPAND_KEY -> keyTextFilter
KEY_PREVIEW -> adjustedBackgroundFilter
KEY_PREVIEW_BACKGROUND -> adjustedBackgroundFilter
ACTION_KEY_ICON -> actionKeyIconColorFilter
else -> colorFilter(get(color))
}
@ -336,7 +336,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
if (view.background == null)
view.setBackgroundColor(Color.WHITE) // set white to make the color filters work
when (color) {
KEY_PREVIEW -> view.background.colorFilter = adjustedBackgroundFilter
KEY_PREVIEW_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
FUNCTIONAL_KEY_BACKGROUND, KEY_BACKGROUND, MORE_SUGGESTIONS_WORD_BACKGROUND, SPACE_BAR_BACKGROUND, STRIP_BACKGROUND -> setColor(view.background, color)
ONE_HANDED_MODE_BUTTON -> setColor(view.background, if (keyboardBackground == null) MAIN_BACKGROUND else STRIP_BACKGROUND)
MORE_SUGGESTIONS_BACKGROUND -> view.background.colorFilter = backgroundFilter
@ -472,10 +472,11 @@ class DefaultColors (
TOOL_BAR_KEY_ENABLED_BACKGROUND, EMOJI_CATEGORY_SELECTED, ACTION_KEY_BACKGROUND,
CLIPBOARD_PIN, SHIFT_KEY_ICON -> accent
AUTOFILL_BACKGROUND_CHIP -> if (themeStyle == STYLE_MATERIAL && !hasKeyBorders) background else adjustedBackground
GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW -> adjustedBackground
GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW_BACKGROUND -> adjustedBackground
TOOL_BAR_EXPAND_KEY_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON -> keyText
KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON, EMOJI_KEY_TEXT,
POPUP_KEY_TEXT, POPUP_KEY_ICON, KEY_PREVIEW_TEXT -> keyText
KEY_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
@ -524,7 +525,7 @@ class DefaultColors (
if (view.background == null)
view.setBackgroundColor(Color.WHITE) // set white to make the color filters work
when (color) {
KEY_PREVIEW, POPUP_KEYS_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
KEY_PREVIEW_BACKGROUND, POPUP_KEYS_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
FUNCTIONAL_KEY_BACKGROUND, KEY_BACKGROUND, MORE_SUGGESTIONS_WORD_BACKGROUND, SPACE_BAR_BACKGROUND, STRIP_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> setColor(view.background, color)
ONE_HANDED_MODE_BUTTON -> setColor(view.background, if (keyboardBackground == null) MAIN_BACKGROUND else STRIP_BACKGROUND)
MORE_SUGGESTIONS_BACKGROUND -> view.background.colorFilter = backgroundFilter
@ -547,7 +548,7 @@ class DefaultColors (
EMOJI_CATEGORY_SELECTED, CLIPBOARD_PIN, SHIFT_KEY_ICON -> accentColorFilter
KEY_TEXT, KEY_ICON -> keyTextFilter
REMOVE_SUGGESTION_ICON, EMOJI_CATEGORY, ONE_HANDED_MODE_BUTTON, TOOL_BAR_KEY, TOOL_BAR_EXPAND_KEY -> suggestionTextFilter
KEY_PREVIEW -> adjustedBackgroundFilter
KEY_PREVIEW_BACKGROUND -> adjustedBackgroundFilter
ACTION_KEY_ICON -> actionKeyIconColorFilter
else -> colorFilter(get(color)) // create color filter (not great for performance, so the frequently used filters should be stored)
}
@ -620,6 +621,7 @@ enum class ColorType {
CLIPBOARD_PIN,
EMOJI_CATEGORY,
EMOJI_CATEGORY_SELECTED,
EMOJI_KEY_TEXT,
FUNCTIONAL_KEY_TEXT,
FUNCTIONAL_KEY_BACKGROUND,
GESTURE_TRAIL,
@ -628,11 +630,14 @@ enum class ColorType {
KEY_ICON,
KEY_TEXT,
KEY_HINT_TEXT,
KEY_PREVIEW,
KEY_PREVIEW_BACKGROUND,
KEY_PREVIEW_TEXT,
MORE_SUGGESTIONS_HINT,
MORE_SUGGESTIONS_BACKGROUND,
MORE_SUGGESTIONS_WORD_BACKGROUND,
POPUP_KEYS_BACKGROUND,
POPUP_KEY_TEXT,
POPUP_KEY_ICON,
NAVIGATION_BAR,
SHIFT_KEY_ICON,
SPACE_BAR_BACKGROUND,

View file

@ -1,56 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.latin.common;
import androidx.annotation.NonNull;
/**
* An immutable class that encapsulates a snapshot of word composition data.
*/
public class ComposedData {
@NonNull
public final InputPointers mInputPointers;
public final boolean mIsBatchMode;
@NonNull
public final String mTypedWord;
public ComposedData(@NonNull final InputPointers inputPointers, final boolean isBatchMode,
@NonNull final String typedWord) {
mInputPointers = inputPointers;
mIsBatchMode = isBatchMode;
mTypedWord = typedWord;
}
/**
* Copy the code points in the typed word to a destination array of ints.
*
* If the array is too small to hold the code points in the typed word, nothing is copied and
* -1 is returned.
*
* @param destination the array of ints.
* @return the number of copied code points.
*/
public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
@NonNull final int[] destination) {
// lastIndex is exclusive
final int lastIndex = mTypedWord.length()
- StringUtils.getTrailingSingleQuotesCount(mTypedWord);
if (lastIndex <= 0) {
// The string is empty or contains only single quotes.
return 0;
}
// The following function counts the number of code points in the text range which begins
// at index 0 and extends to the character at lastIndex.
final int codePointSize = Character.codePointCount(mTypedWord, 0, lastIndex);
if (codePointSize > destination.length) {
return -1;
}
return StringUtils.copyCodePointsAndReturnCodePointCount(destination, mTypedWord, 0,
lastIndex, true /* downCase */);
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (C) 2014 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.latin.common
import helium314.keyboard.latin.WordComposer
import kotlin.random.Random
/** An immutable class that encapsulates a snapshot of word composition data. */
class ComposedData(
@JvmField val mInputPointers: InputPointers,
@JvmField val mIsBatchMode: Boolean,
@JvmField val mTypedWord: String
) {
/**
* Copy the code points in the typed word to a destination array of ints.
*
* If the array is too small to hold the code points in the typed word, nothing is copied and
* -1 is returned.
*
* @param destination the array of ints.
* @return the number of copied code points.
*/
fun copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
destination: IntArray
): Int {
// lastIndex is exclusive
val lastIndex = (mTypedWord.length - StringUtils.getTrailingSingleQuotesCount(mTypedWord))
if (lastIndex <= 0) {
return 0 // The string is empty or contains only single quotes.
}
// The following function counts the number of code points in the text range which begins
// at index 0 and extends to the character at lastIndex.
val codePointSize = Character.codePointCount(mTypedWord, 0, lastIndex)
if (codePointSize > destination.size) {
return -1
}
return StringUtils.copyCodePointsAndReturnCodePointCount(
destination, mTypedWord, 0, lastIndex, true
)
}
companion object {
fun createForWord(word: String): ComposedData {
val codePoints = StringUtils.toCodePointArray(word)
val coordinates = CoordinateUtils.newCoordinateArray(codePoints.size)
for (i in codePoints.indices) {
CoordinateUtils.setXYInArray(coordinates, i, Random.nextBits(2), Random.nextBits(2))
}
return WordComposer().apply { setComposingWord(codePoints, coordinates) }.composedDataSnapshot
}
}
}

View file

@ -197,6 +197,8 @@ public final class Constants {
public static final int CODE_GRAVE_ACCENT = '`';
public static final int CODE_CIRCUMFLEX_ACCENT = '^';
public static final int CODE_TILDE = '~';
public static final int RECENTS_TEMPLATE_KEY_CODE_0 = 0x30;
public static final int RECENTS_TEMPLATE_KEY_CODE_1 = 0x31;
public static final String REGEXP_PERIOD = "\\.";
public static final String STRING_SPACE = " ";

View file

@ -8,10 +8,10 @@ package helium314.keyboard.latin.common
import android.content.Context
import android.content.res.Resources
import helium314.keyboard.compat.locale
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.runInLocale
import java.util.Locale
/**
@ -171,34 +171,32 @@ object LocaleUtils {
}
}
@JvmStatic
fun isRtlLanguage(locale: Locale): Boolean {
val displayName = locale.getDisplayName(locale)
if (displayName.isEmpty()) return false
return when (Character.getDirectionality(displayName.codePointAt(0))) {
Character.DIRECTIONALITY_RIGHT_TO_LEFT, Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC -> true
else -> false
}
}
fun Locale.localizedDisplayName(resources: Resources, displayLocale: Locale? = null): String {
val languageTag = toLanguageTag()
if (languageTag == SubtypeLocaleUtils.NO_LANGUAGE)
return resources.getString(R.string.subtype_no_language)
fun Locale.localizedDisplayName(context: Context) =
getLocaleDisplayNameInLocale(this, context.resources, context.resources.configuration.locale())
@JvmStatic
fun getLocaleDisplayNameInLocale(locale: Locale, resources: Resources, displayLocale: Locale): String {
val languageTag = locale.toLanguageTag()
if (languageTag == SubtypeLocaleUtils.NO_LANGUAGE) return resources.getString(R.string.subtype_no_language)
if (locale.script() != locale.language.constructLocale().script() || locale.language == "mns" || locale.language == "xdq" || locale.language=="dru") {
val resId = resources.getIdentifier(
"subtype_${languageTag.replace("-", "_")}",
"string",
BuildConfig.APPLICATION_ID // replaces context.packageName, see https://stackoverflow.com/a/24525379
)
if (resId != 0) return resources.getString(resId)
val overrideResId = when (languageTag) {
"en-US" -> R.string.subtype_en_US
"en-GB" -> R.string.subtype_en_GB
"es-US" -> R.string.subtype_es_US
"hi-Latn" -> R.string.subtype_hi_Latn
"sr-Latn" -> R.string.subtype_sr_Latn
"mns" -> R.string.subtype_mns
"xdq" -> R.string.subtype_xdq
"dru" -> R.string.subtype_xdq
"st" -> R.string.subtype_st
"dag" -> R.string.subtype_dag
else -> 0
}
val localeDisplayName = locale.getDisplayName(displayLocale)
if (overrideResId != 0) {
return if (displayLocale == null) resources.getString(overrideResId)
else runInLocale(resources, displayLocale) { it.getString(overrideResId) }
}
val localeDisplayName = getDisplayName(displayLocale ?: resources.configuration.locale())
return if (localeDisplayName == languageTag) {
locale.getDisplayName(Locale.US) // try fallback to English name, relevant e.g. fpr pms, see https://github.com/Helium314/HeliBoard/pull/748
getDisplayName(Locale.US) // try fallback to English name, relevant e.g. fpr pms, see https://github.com/Helium314/HeliBoard/pull/748
} else {
localeDisplayName
}

View file

@ -6,67 +6,66 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.common.StringUtils.mightBeEmoji
import helium314.keyboard.latin.common.StringUtils.newSingleCodePointString
import helium314.keyboard.latin.settings.SpacingAndPunctuations
import helium314.keyboard.latin.utils.SpacedTokens
import java.math.BigInteger
import java.util.Locale
fun loopOverCodePoints(s: CharSequence, run: (Int) -> Boolean) {
val text = if (s is String) s else s.toString()
fun CharSequence.codePointAt(offset: Int) = Character.codePointAt(this, offset)
fun CharSequence.codePointBefore(offset: Int) = Character.codePointBefore(this, offset)
inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
var offset = 0
while (offset < text.length) {
val codepoint = text.codePointAt(offset)
if (run(codepoint)) return
offset += Character.charCount(codepoint)
val cp = text.codePointAt(offset)
val charCount = Character.charCount(cp)
if (loop(cp, charCount)) return
offset += charCount
}
}
fun loopOverCodePointsBackwards(s: CharSequence, run: (Int) -> Boolean) {
val text = if (s is String) s else s.toString()
inline fun loopOverCodePointsBackwards(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
var offset = text.length
while (offset > 0) {
val codepoint = text.codePointBefore(offset)
if (run(codepoint)) return
offset -= Character.charCount(codepoint)
val cp = text.codePointBefore(offset)
val charCount = Character.charCount(cp)
if (loop(cp, charCount)) return
offset -= charCount
}
}
fun nonWordCodePointAndNoSpaceBeforeCursor(s: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
fun nonWordCodePointAndNoSpaceBeforeCursor(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
var space = false
var nonWordCodePoint = false
loopOverCodePointsBackwards(s) {
if (!space && Character.isWhitespace(it))
space = true
// treat double quote like a word codepoint for the purpose of this function (not great, maybe clarify name, or extend list of chars?)
if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(it) && it != '"'.code)
loopOverCodePointsBackwards(text) { cp, _ ->
if (!space && Character.isWhitespace(cp)) space = true
// treat double quote like a word codepoint for this function (not great, maybe clarify name or extend list of chars?)
if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(cp) && cp != '"'.code) {
nonWordCodePoint = true
}
space && nonWordCodePoint // stop if both are found
}
return nonWordCodePoint && !space // return true if an non-word codepoint and no space was found
return nonWordCodePoint && !space // return true if a non-word codepoint and no space was found
}
fun hasLetterBeforeLastSpaceBeforeCursor(s: CharSequence): Boolean {
var letter = false
loopOverCodePointsBackwards(s) {
if (Character.isWhitespace(it)) true
else if (Character.isLetter(it)) {
letter = true
true
}
else false
fun hasLetterBeforeLastSpaceBeforeCursor(text: CharSequence): Boolean {
loopOverCodePointsBackwards(text) { cp, _ ->
if (Character.isWhitespace(cp)) return false
else if (Character.isLetter(cp)) return true
false // continue
}
return letter
return false
}
/** get the complete emoji at end of [s], considering that emojis can be joined with ZWJ resulting in different emojis */
fun getFullEmojiAtEnd(s: CharSequence): String {
val text = if (s is String) s else s.toString()
var offset = text.length
/** get the complete emoji at end of [text], considering that emojis can be joined with ZWJ resulting in different emojis */
fun getFullEmojiAtEnd(text: CharSequence): String {
val s = text.toString()
var offset = s.length
while (offset > 0) {
val codepoint = text.codePointBefore(offset)
val codepoint = s.codePointBefore(offset)
// stop if codepoint can't be emoji
if (!mightBeEmoji(codepoint))
return text.substring(offset)
if (!mightBeEmoji(codepoint)) return text.substring(offset)
offset -= Character.charCount(codepoint)
if (offset > 0 && text[offset - 1].code == KeyCode.ZWJ) {
if (offset > 0 && s[offset - 1].code == KeyCode.ZWJ) {
// todo: this appends ZWJ in weird cases like text, ZWJ, emoji
// and detects single ZWJ as emoji (at least irrelevant for current use of getFullEmojiAtEnd)
offset -= 1
@ -76,19 +75,17 @@ fun getFullEmojiAtEnd(s: CharSequence): String {
if (codepoint in 0x1F3FB..0x1F3FF) {
// Skin tones are not added with ZWJ, but just appended. This is not nice as they can be emojis on their own,
// but that's how it is done. Assume that an emoji before the skin tone will get merged (usually correct in practice)
val codepointBefore = text.codePointBefore(offset)
val codepointBefore = s.codePointBefore(offset)
if (isEmoji(codepointBefore)) {
offset -= Character.charCount(codepointBefore)
continue
}
}
// check the whole text after offset
val textToCheck = text.substring(offset)
if (isEmoji(textToCheck)) {
return textToCheck
}
val textToCheck = s.substring(offset)
if (isEmoji(textToCheck)) return textToCheck
}
return text.substring(offset)
return s.substring(offset)
}
/** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
@ -110,8 +107,7 @@ fun String.splitOnFirstSpacesOnly(): List<String> {
sb.append(c)
}
}
if (sb.isNotBlank())
out.add(sb.toString())
if (sb.isNotBlank()) out.add(sb.toString())
return out
}
@ -120,8 +116,7 @@ fun CharSequence.isValidNumber(): Boolean {
}
fun String.decapitalize(locale: Locale): String {
if (isEmpty() || !this[0].isUpperCase())
return this
if (isEmpty() || !this[0].isUpperCase()) return this
return replaceFirstChar { it.lowercase(locale) }
}
@ -136,11 +131,9 @@ fun containsValueWhenSplit(string: String?, value: String, split: String): Boole
fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c))
fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex)
fun isEmoji(text: CharSequence): Boolean = mightBeEmoji(text) && text.matches(emoRegex)
fun String.splitOnWhitespace() = split(whitespaceSplitRegex)
private val whitespaceSplitRegex = "\\s+".toRegex()
fun String.splitOnWhitespace() = SpacedTokens(this).toList()
// from https://github.com/mathiasbynens/emoji-test-regex-pattern, MIT license
// matches single emojis only

View file

@ -11,6 +11,7 @@ import android.os.Build
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.settings.DebugSettings
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.prefs
import java.io.File
@ -64,11 +65,17 @@ ${Log.getLog(100).joinToString("\n")}
private fun writeCrashReportToFile(text: String) {
try {
val dir = appContext.getExternalFilesDir(null) ?: return
val dir = appContext.getExternalFilesDir(null)
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Calendar.getInstance().time)
val crashReportFile = File(dir, "crash_report_$date.txt")
crashReportFile.writeText(text)
} catch (ignored: IOException) {
crashReportFile.appendText(text)
} catch (_: Exception) {
// can't write in external files dir, maybe device just booted and is still locked
// in this case there shouldn't be any sensitive data and we can put crash logs in unprotected files dir
val dir = DeviceProtectedUtils.getFilesDir(appContext) ?: return
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Calendar.getInstance().time)
val crashReportFile = File(dir, "crash_report_unprotected_$date.txt")
crashReportFile.appendText(text)
}
}
}

View file

@ -54,6 +54,7 @@ import helium314.keyboard.latin.utils.RecapitalizeStatus;
import helium314.keyboard.latin.utils.ScriptUtils;
import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.TextRange;
import helium314.keyboard.latin.utils.TimestampKt;
import java.util.ArrayList;
import java.util.Locale;
@ -323,7 +324,8 @@ public final class InputLogic {
// Don't allow cancellation of manual pick
mLastComposedWord.deactivate();
// Space state must be updated before calling updateShiftState
mSpaceState = SpaceState.PHANTOM;
if (settingsValues.mAutospaceAfterSuggestion)
mSpaceState = SpaceState.PHANTOM;
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
// If we're not showing the "Touch again to save", then update the suggestion strip.
@ -438,19 +440,30 @@ public final class InputLogic {
mWordBeingCorrectedByCursor = null;
mJustRevertedACommit = false;
final Event processedEvent;
if (currentKeyboardScript.equals(ScriptUtils.SCRIPT_HANGUL)
// only use the Hangul chain if codepoint may actually be Hangul
// todo: this whole hangul-related logic should probably be somewhere else
// need to use hangul combiner for whitespace, because otherwise the current word
// seems to get deleted / replaced by space during mConnection.endBatchEdit()
// similar for functional keys (codePoint -1)
&& (event.getMCodePoint() >= 0x1100 || Character.isWhitespace(event.getMCodePoint()) || event.getMCodePoint() == -1)) {
mWordComposer.setHangul(true);
final Event hangulDecodedEvent = HangulEventDecoder.decodeSoftwareKeyEvent(event);
// todo: here hangul combiner does already consume the event, and appends typed codepoint
// to the current word instead of considering the cursor position
// position is actually not visible to the combiner, how to fix?
processedEvent = mWordComposer.processEvent(hangulDecodedEvent);
if (currentKeyboardScript.equals(ScriptUtils.SCRIPT_HANGUL)) {
// only use the Hangul chain if codepoint may actually be Hangul
// todo: this whole hangul-related logic should probably be somewhere else
// need to use hangul combiner for functional keys (codePoint -1), because otherwise the current word
// seems to get deleted / replaced by space during mConnection.endBatchEdit()
if (event.getMCodePoint() >= 0x1100 || event.getMCodePoint() == -1) {
mWordComposer.setHangul(true);
final Event hangulDecodedEvent = HangulEventDecoder.decodeSoftwareKeyEvent(event);
// todo: here hangul combiner does already consume the event, and appends typed codepoint
// to the current word instead of considering the cursor position
// position is actually not visible to the combiner, how to fix?
processedEvent = mWordComposer.processEvent(hangulDecodedEvent);
if (event.getMKeyCode() == KeyCode.DELETE)
mWordComposer.resetInvalidCursorPosition();
} else {
mWordComposer.setHangul(false);
final boolean wasComposingWord = mWordComposer.isComposingWord();
processedEvent = mWordComposer.processEvent(event);
// workaround for space and some other separators deleting / replacing the word
if (wasComposingWord && !mWordComposer.isComposingWord()) {
mWordComposer.resetInvalidCursorPosition();
mConnection.finishComposingText();
}
}
} else {
mWordComposer.setHangul(false);
processedEvent = mWordComposer.processEvent(event);
@ -546,7 +559,8 @@ public final class InputLogic {
|| settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() !=
getCurrentAutoCapsState(settingsValues);
mSpaceState = SpaceState.PHANTOM;
if (settingsValues.mAutospaceBeforeGestureTyping)
mSpaceState = SpaceState.PHANTOM;
if (!autoShiftHasBeenOverriden) {
// When we change the space state, we need to update the shift state of the
// keyboard unless it has been overridden manually. This is happening for example
@ -686,10 +700,7 @@ public final class InputLogic {
if (mSuggestedWords.isPrediction()) {
inputTransaction.setRequiresUpdateSuggestions();
}
// undo phantom space if it's because after punctuation
// users who want to start a sentence with a lowercase letter may not like it
if (mSpaceState == SpaceState.PHANTOM
&& inputTransaction.getMSettingsValues().isUsuallyFollowedBySpace(mConnection.getCodePointBeforeCursor()))
if (mSpaceState == SpaceState.PHANTOM && inputTransaction.getMSettingsValues().mShiftRemovesAutospace)
mSpaceState = SpaceState.NONE;
break;
case KeyCode.SETTINGS:
@ -756,16 +767,35 @@ public final class InputLogic {
}
break;
case KeyCode.WORD_LEFT:
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_CTRL_ON);
sendDownUpKeyEventWithMetaState(ScriptUtils.isScriptRtl(currentKeyboardScript)?
KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_CTRL_ON);
break;
case KeyCode.WORD_RIGHT:
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_CTRL_ON);
sendDownUpKeyEventWithMetaState(ScriptUtils.isScriptRtl(currentKeyboardScript)?
KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_CTRL_ON);
break;
case KeyCode.MOVE_START_OF_PAGE:
final int selectionEnd = mConnection.getExpectedSelectionEnd();
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON);
if (mConnection.getExpectedSelectionStart() > 0 && mConnection.getExpectedSelectionEnd() == selectionEnd) {
// unchanged, and we're not at the top -> try a different method (necessary for compose fields)
mConnection.setSelection(0, 0);
}
break;
case KeyCode.MOVE_END_OF_PAGE:
final int selectionStart = mConnection.getExpectedSelectionEnd();
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON);
if (mConnection.getExpectedSelectionStart() == selectionStart) {
// unchanged, try fallback e.g. for compose fields that don't care about ctrl + end
// we just move to a very large index, and hope the field is prepared to deal with this
// getting the actual length of the text for setting the correct position can be tricky for some apps...
try {
mConnection.setSelection(Integer.MAX_VALUE, Integer.MAX_VALUE);
} catch (Exception e) {
// better catch potential errors and just do nothing in this case
Log.i(TAG, "error when trying to move cursor to last position: " + e);
}
}
break;
case KeyCode.UNDO:
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON);
@ -776,14 +806,18 @@ public final class InputLogic {
case KeyCode.SPLIT_LAYOUT:
KeyboardSwitcher.getInstance().toggleSplitKeyboardMode();
break;
case KeyCode.TIMESTAMP:
mLatinIME.onTextInput(TimestampKt.getTimestamp(mLatinIME));
break;
case KeyCode.VOICE_INPUT:
// switching to shortcut IME, shift state, keyboard,... is handled by LatinIME,
// {@link KeyboardSwitcher#onEvent(Event)}, or {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
// We need to switch to the shortcut IME. This is handled by LatinIME since the
// input logic has no business with IME switching.
case KeyCode.CAPS_LOCK, KeyCode.SYMBOL_ALPHA, KeyCode.ALPHA, KeyCode.SYMBOL, KeyCode.NUMPAD, KeyCode.EMOJI,
KeyCode.TOGGLE_ONE_HANDED_MODE, KeyCode.SWITCH_ONE_HANDED_MODE,
KeyCode.CTRL, KeyCode.ALT, KeyCode.FN, KeyCode.META:
KeyCode.TOGGLE_ONE_HANDED_MODE, KeyCode.SWITCH_ONE_HANDED_MODE, KeyCode.FN,
KeyCode.CTRL, KeyCode.CTRL_LEFT, KeyCode.CTRL_RIGHT, KeyCode.ALT, KeyCode.ALT_LEFT, KeyCode.ALT_RIGHT,
KeyCode.META, KeyCode.META_LEFT, KeyCode.META_RIGHT:
break;
default:
if (event.getMMetaState() != 0) {
@ -930,6 +964,7 @@ public final class InputLogic {
// handleNonSpecialCharacterEvent which has the same name as other handle* methods but is
// not the same.
boolean isComposingWord = mWordComposer.isComposingWord();
mWordComposer.unsetBatchMode(); // relevant in case we continue a batch word with normal typing
// if we continue directly after a sometimesWordConnector, restart suggestions for the whole word
// (only with URL detection and suggestions enabled)
@ -949,7 +984,9 @@ public final class InputLogic {
// TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
// See onStartBatchInput() to see how to do it.
if (SpaceState.PHANTOM == inputTransaction.getMSpaceState()
&& !settingsValues.isWordConnector(codePoint)) {
&& !settingsValues.isWordConnector(codePoint)
&& !settingsValues.isUsuallyFollowedBySpace(codePoint) // only relevant in rare cases
) {
if (isComposingWord) {
// Sanity check
throw new RuntimeException("Should not be composing here");
@ -1127,7 +1164,7 @@ public final class InputLogic {
// A double quote behaves like it's usually followed by space if we're inside
// a double quote.
if (wasComposingWord
&& settingsValues.mAutospaceAfterPunctuationEnabled
&& settingsValues.mAutospaceAfterPunctuation
&& (settingsValues.isUsuallyFollowedBySpace(codePoint) || isInsideDoubleQuoteOrAfterDigit)) {
mSpaceState = SpaceState.PHANTOM;
}
@ -1196,7 +1233,7 @@ public final class InputLogic {
}
inputTransaction.setRequiresUpdateSuggestions();
} else {
if (mLastComposedWord.canRevertCommit()) {
if (mLastComposedWord.canRevertCommit() && inputTransaction.getMSettingsValues().mBackspaceRevertsAutocorrect) {
final String lastComposedWord = mLastComposedWord.mTypedWord;
revertCommit(inputTransaction);
StatsUtils.onRevertAutoCorrect();
@ -2168,6 +2205,7 @@ public final class InputLogic {
&& !(mConnection.getCodePointBeforeCursor() == Constants.CODE_PERIOD && mConnection.wordBeforeCursorMayBeEmail())
) {
mConnection.commitCodePoint(Constants.CODE_SPACE);
// todo: why not remove phantom space state?
}
}
@ -2202,12 +2240,14 @@ public final class InputLogic {
mConnection.beginBatchEdit();
if (SpaceState.PHANTOM == mSpaceState) {
insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues);
mSpaceState = SpaceState.NONE;
}
mWordComposer.setBatchInputWord(batchInputText);
setComposingTextInternal(batchInputText, 1);
mConnection.endBatchEdit();
// Space state must be updated before calling updateShiftState
mSpaceState = SpaceState.PHANTOM;
if (settingsValues.mAutospaceAfterGestureTyping)
mSpaceState = SpaceState.PHANTOM;
keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState());
}

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