Compare commits

...

253 commits

Author SHA1 Message Date
Eran Leshem
d2217f099a
Export DictionaryPackInstallBroadcastReceiver (#1756) 2025-06-29 20:02:29 +02:00
Eran Leshem
4f58e5d013
Support Latin (#1749) 2025-06-28 16:25:15 +02:00
Helium314
10a5eab3bc don't change insets when view is hidden by hardware keyboard
seems to happen only in some apps, e.g. simplemobiletools notes
issue only occurs when keyboard is suppressed after it has been shown and a new input view is created

fixes GH-1455
fixes GH-702
2025-06-28 16:20:54 +02:00
Helium314
8d876a15f0 fix crash in isShowingKeyboardId with hardware keyboard 2025-06-28 15:16:25 +02:00
Helium314
945a700b71 do the correct checks when loading blacklist file
fixes GH-1475
2025-06-28 14:11:00 +02:00
Helium314
53a899794e register OnSharedPreferenceChangeListener in LatinIME.onCreate
because it's unregistered in onDestroy
(registering twice is not an issue)

though maybe not unregistering would be more correct, as it's registered in app.onCreate?

fixes GH-1670
2025-06-24 06:27:51 +02:00
Eran Leshem
c2068224a0
Change settings search keyboard action to Search. (#1734) 2025-06-22 12:23:46 +02:00
Eran Leshem
9193c95c2b
Ignore spacer columns when calculating emoji page size (#1721) 2025-06-22 12:18:37 +02:00
Helium314
76ebf99921 deal with Android refusing to add word to user dictionary
fixes GH-1735
2025-06-22 12:11:20 +02:00
Helium314
77a728e390 don't pre-select unavailable locale when adding dictionary 2025-06-19 09:35:08 +02:00
Helium314
7b0c511857 add logging when hiding window
may help with GH-1710
2025-06-16 19:43:32 +02:00
Eran Leshem
e062efb3d4
Always default popup VisibleOffset to keyboard.mVerticalGap (#1722) 2025-06-16 19:14:47 +02:00
lurebat
d356f9f54b
Add broadcast intent keys (#1675)
This commit introduces three new keycodes: SEND_INTENT_ONE, SEND_INTENT_TWO, and SEND_INTENT_THREE.

When these keys are pressed, a broadcast intent is sent with the action `helium314.keyboard.latin.ACTION_SEND_INTENT`. The intent includes an extra `EXTRA_NUMBER` (integer) indicating which of the three keys was pressed (1, 2, or 3).

This functionality allows external applications to react to these specific key presses.
2025-06-16 18:55:15 +02:00
Helium314
9549389be7 use different method for hiding keyboard
fixes GH-1719
2025-06-15 21:09:53 +02:00
Helium314
2dc838798d use hasLabels for TLD popups
and add documentation
2025-06-15 19:49:32 +02:00
Helium314
24a2eddc1f avoid StringIndexOutOfBoundsException when initializing stringbuilder from other stringbuilder 2025-06-15 17:43:06 +02:00
Helium314
ef3191a2eb prepare for selecting text with cursor movement keys when shift is enabled manually
not enabled, as it's not working reliably
2025-06-15 17:41:05 +02:00
Helium314
e430d13c4a improvements to hardware keyboard handling 2025-06-14 12:53:46 +02:00
Helium314
9c97a6b9bf move some key press logic from LatinIme into KeyboardActionListener 2025-06-14 12:16:29 +02:00
Helium314
a37de668c0 enable hardware keyboard handling again, with improved handling 2025-06-14 11:29:26 +02:00
Helium314
63dad1549e redirect suggestion strip view code input to KeyboardActionListener 2025-06-13 20:57:49 +02:00
Helium314
f06a553d2c make sure selection start is before end
this should not be necessary according to documentation, but apparently there are apps (even by Google!) that can't deal with documented behavior
see comments in GH-1512
2025-06-13 20:49:53 +02:00
Eran Leshem
e9e3bdac17
Display emoji descriptions in popups (#1542) 2025-06-11 22:17:26 +02:00
Eran Leshem
d5cd18ecaa
Remove regional indicator symbol letters from emoji list (#1680) 2025-06-11 22:17:05 +02:00
Eran Leshem
871ac110ad
Move non-com global TLDs to end of list (#1668) 2025-06-11 22:16:39 +02:00
Helium314
49c9d77978 try more aggressive proguard settings 2025-06-11 22:15:49 +02:00
Eran Leshem
79726f1a9d
Fix onInlineSuggestionsResponse crash (#1700) 2025-06-11 21:59:36 +02:00
Helium314
f2ec441f45 update version and translations 2025-06-11 21:57:06 +02:00
Eran Leshem
62f82d15cf
Show incognito button in all toolbar modes (#1681) 2025-06-11 21:43:04 +02:00
Eran Leshem
f8d3795302
Disable emoji view animation according to system setting (#1696) 2025-06-11 21:29:06 +02:00
Helium314
9cec401e1e don't add single letter words with AddToPersonalDictionary setting
fixes GH-1605
2025-06-11 20:01:59 +02:00
Helium314
83ff9b3345 bring up keyboard when adding a new word in personal dictionary screen
fixes GH-1663
2025-06-11 19:43:42 +02:00
Eran Leshem
8ae241b032
Only show some suggestion settings if toolbar is in compatible mode. (#1693) 2025-06-11 17:24:40 +02:00
Helium314
80ba394b95 move some code from RichInputConnection to StringUtils
so we can easily add unit tests
and maybe improve the awkward behavior
2025-06-09 20:20:27 +02:00
Helium314
52744b7427 full reload after changing icon style
should fix GH-1686
2025-06-09 16:05:41 +02:00
Helium314
af5c41c83c add NO_LANGUAGE inside remember
should fix GH-1678
2025-06-08 21:42:33 +02:00
Helium314
e21168b1d3 merge labelFlags when when replacing comma & period
so e.g. dvorak z key doesn't get .com popup hint by default
2025-06-08 11:40:38 +02:00
Helium314
11f45a6209 show PREF_EMOJI_KEY_FIT setting only if PREF_EMOJI_FONT_SCALE is not 100% 2025-06-08 11:26:23 +02:00
Helium314
c8322dd4a2 make KeyCode.IME_HIDE_UI work
fixes GH-1272
2025-06-08 11:16:00 +02:00
Helium314
896e207c5f remove the fallback to original key type when replacing comma & period
relevant e.g. for dvorak in url fields, where otherwise the q has functional key background
2025-06-08 11:07:51 +02:00
Helium314
5cd184e5c2 upgrade version 2025-06-07 23:04:18 +02:00
fakerat
e98c3210dc
fix forced dark mode issue in android 10 or later (#1671) 2025-06-07 23:03:21 +02:00
Helium314
7cec6b148c remove en-rXC files 2025-06-07 22:35:40 +02:00
Helium314
bd85498810 remove useless translations of strings marked as not translatable (because they reference other strings) 2025-06-07 22:31:48 +02:00
Md. Rifat Hasan Jihan
51a863d840
remove non-translatable string (#1651) 2025-06-07 21:59:12 +02:00
Helium314
defec4a27f update translations 2025-06-07 21:58:25 +02:00
Helium314
ccc287c4ea use CombiningRules extra value for setting the combiner
allows flexibility which is needed for the combiner suggested in GH-214
2025-06-07 21:44:14 +02:00
Helium314
48f6c21b57 fix recognition of our own inputMethod 2025-06-07 21:01:48 +02:00
Eran Leshem
9efe534c03
Fix locale_key_texts loading for he locale (#1669) 2025-06-07 20:38:38 +02:00
Eran Leshem
16ce183942
Fix NextScreenIcon direction for RTL system languages (#1667) 2025-06-07 20:34:47 +02:00
Helium314
867438fdc0 simplify the way of setting suggestions
remove the excursion to latinIME
have Suggest return suggestions instead of calling a callback
now the relevant code can be mostly found in inputLogic (except for handling batch input)
2025-06-07 20:26:06 +02:00
BlackyHawky
0787a79de4
Slightly adjust the dictionary dialog (#1656) 2025-06-03 19:27:01 +02:00
Helium314
97aec851e4 avoid dictionary re-ordering on UI interaction 2025-06-02 20:41:52 +02:00
Helium314
7dbf5ea86d
Add MultiSliderDialog (#1580) 2025-06-02 20:33:10 +02:00
Helium314
d951a1dbdd don't try to back up directories matching the file name patterns
fixes #1653
2025-06-02 17:36:06 +02:00
BlackyHawky
20d4704090
Fix unnecessary indentations in the Subtype screen (#1516) (#1648) 2025-06-02 17:12:11 +02:00
Helium314
d0787201ae get rid of warnings in SuggestionStripView
and add a comment
2025-06-01 19:53:56 +02:00
Helium314
0a961a68db consider that voice input key visibility should depend on the text field 2025-06-01 19:17:44 +02:00
Helium314
7499c38e13 add missing key codes to Key.isModifier, and move the information into KeyCode 2025-06-01 18:40:54 +02:00
Helium314
f81f6a7f7d some additional safety, so we show at least a working keyboard in case dictionary or native library loading fails 2025-06-01 17:03:16 +02:00
Helium314
07ea14ea16 move code related to showing more suggestions into MoreSuggestionsView 2025-06-01 16:38:59 +02:00
Helium314
32099748e5 move PopupSuggestionsView to Kotlin
and rename to MoreSuggestionsView
2025-06-01 13:36:14 +02:00
Helium314
538a26a9b8 no need to provide KeyboardIconsSet when creating a toolbar key 2025-06-01 13:21:03 +02:00
Helium314
8284adb32c small cleanup in ClipbardHistoryView 2025-06-01 13:19:38 +02:00
Helium314
180bd179c5 move device locked check to common place 2025-06-01 13:13:00 +02:00
Helium314
12c1cb0cd2 move SuggestionStripView to Kotlin 2025-06-01 13:06:14 +02:00
Eran Leshem
ec2bbb461d
Add toolbar modes (#1606) 2025-06-01 09:50:55 +02:00
Helium314
38bbcd9a83 add editorconfig, udpate gitignore 2025-06-01 09:50:24 +02:00
Helium314
8a55f57ccf decrease minimum height scale to 30%
resolves GH-1647
2025-05-31 20:46:32 +02:00
Helium314
e01714bbee mark string as not translatable 2025-05-31 20:39:11 +02:00
Helium314
3fcea486be gather crash information more aggressively
especially for crashes while device is locked
might help with #801
2025-05-31 20:30:18 +02:00
Helium314
e4e99a7e5d fix NPE related to recent RichIMM changes 2025-05-31 14:28:02 +02:00
Helium314
c321c2c684 replace AsyncTask 2025-05-31 14:01:36 +02:00
Helium314
7dc6279d87 todos after RichIMM overhaul 2025-05-31 13:56:02 +02:00
Helium314
ad71b49c04 make language slide work with fallback subtypes 2025-05-31 13:31:22 +02:00
Helium314
09eecc0502 move RichInputMethodManager to Kotlin 2025-05-31 13:22:22 +02:00
Helium314
424df5fb0d fix file handling in DictionaryGroup (again) 2025-05-31 11:59:05 +02:00
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
439 changed files with 14090 additions and 13054 deletions

13
.editorconfig Normal file
View file

@ -0,0 +1,13 @@
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 140
tab_width = 4
trim_trailing_whitespace = true
[{*.markdown,*.md}]
trim_trailing_whitespace = false
indent_size = 2

2
.gitignore vendored
View file

@ -1,6 +1,7 @@
*.iml
.idea
.gradle
.kotlin
local.properties
.DS_Store
Gemfile
@ -8,5 +9,6 @@ build
app/build
app/release
app/.cxx
app/.attach_*
fastlane/Appfile
tools/*.txt

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,25 +1,24 @@
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 = 3201
versionName = "3.2"
ndk {
abiFilters.clear()
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))
}
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
buildTypes {
@ -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,224 @@
🏧
🚮
🚰
🚹
🚺
🚻
🚼
🚾
🛂
🛃
🛄
🛅
⚠️
🚸
🚫
🚳
🚭
🚯
🚱
🚷
📵
🔞
☢️
☣️
⬆️
↗️
➡️
↘️
⬇️
↙️
⬅️
↖️
↕️
↔️
↩️
↪️
⤴️
⤵️
🔃
🔄
🔙
🔚
🔛
🔜
🔝
🛐
⚛️
🕉️
✡️
☸️
☯️
✝️
☦️
☪️
☮️
🕎
🔯
🪯
🔀
🔁
🔂
▶️
⏭️
⏯️
◀️
⏮️
🔼
🔽
⏸️
⏹️
⏺️
⏏️
🎦
🔅
🔆
📶
🛜
📳
📴
♀️
♂️
⚧️
✖️
🟰
♾️
‼️
⁉️
〰️
💱
💲
⚕️
♻️
⚜️
🔱
📛
🔰
☑️
✔️
〽️
✳️
✴️
❇️
©️
®️
™️
🫟
#️⃣
*️⃣
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

@ -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

@ -70,7 +70,12 @@ public class ProximityInfo {
return;
}
computeNearestNeighbors();
mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection);
try {
mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection);
} catch (Throwable e) {
Log.e(TAG, "could not create proximity info", e);
mNativeProximityInfo = 0;
}
}
private long mNativeProximityInfo;

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

@ -0,0 +1,13 @@
package helium314.keyboard.compat
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
fun isDeviceLocked(context: Context): Boolean {
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
keyguardManager.isDeviceLocked
else
keyguardManager.isKeyguardLocked
}

View file

@ -29,30 +29,18 @@ import java.util.*
* cursor: we'll start after this.
* @param initialText The text that has already been combined so far.
*/
class CombinerChain(initialText: String) {
class CombinerChain(initialText: String, combiningSpec: String) {
// The already combined text, as described above
private val mCombinedText = StringBuilder(initialText)
// The feedback on the composing state, as described above
private val mStateFeedback = SpannableStringBuilder()
private val mCombiners = ArrayList<Combiner>()
// Hangul combiner affects other scripts, e.g. period is seen as port of a word for latin,
// so we need to remove the combiner when not writing in hangul script.
// Maybe it would be better to always have the Hangul combiner, but make sure it doesn't affect
// events for other scripts, but how?
// todo: this really should be done properly, hangul combiner should do nothing when it's not needed
var isHangul = false
set(value) {
if (field == value) return
field = value
if (!value)
mCombiners.removeAll { it is HangulCombiner }
else if (mCombiners.none { it is HangulCombiner })
mCombiners.add(HangulCombiner())
}
init {
// The dead key combiner is always active, and always first
mCombiners.add(DeadKeyCombiner())
if (combiningSpec == "hangul")
mCombiners.add(HangulCombiner())
}
fun reset() {

View file

@ -139,6 +139,16 @@ class Event private constructor(
null, if (isKeyRepeat) FLAG_REPEAT else FLAG_NONE, null)
}
// A helper method to split the code point and the key code.
// todo: Ultimately, they should not be squashed into the same variable, and this method should be removed.
@JvmStatic
fun createSoftwareKeypressEvent(keyCodeOrCodePoint: Int, metaState: Int, keyX: Int, keyY: Int, isKeyRepeat: Boolean) =
if (keyCodeOrCodePoint <= 0) {
createSoftwareKeypressEvent(NOT_A_CODE_POINT, keyCodeOrCodePoint, metaState, keyX, keyY, isKeyRepeat)
} else {
createSoftwareKeypressEvent(keyCodeOrCodePoint, NOT_A_KEY_CODE, metaState, keyX, keyY, isKeyRepeat)
}
fun createHardwareKeypressEvent(codePoint: Int, keyCode: Int, metaState: Int, next: Event?, isKeyRepeat: Boolean): Event {
return Event(EVENT_TYPE_INPUT_KEYPRESS, null, codePoint, keyCode, metaState,
Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE,
@ -256,10 +266,8 @@ class Event private constructor(
source.mX, source.mY, source.mSuggestedWordInfo, source.mFlags or FLAG_COMBINING, source.mNextEvent)
}
fun createNotHandledEvent(): Event {
return Event(EVENT_TYPE_NOT_HANDLED, null, NOT_A_CODE_POINT, NOT_A_KEY_CODE, 0,
val notHandledEvent = Event(EVENT_TYPE_NOT_HANDLED, null, NOT_A_CODE_POINT, NOT_A_KEY_CODE, 0,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, null, FLAG_NONE, null)
}
}
// This method is private - to create a new event, use one of the create* utility methods.

View file

@ -16,6 +16,8 @@ class HangulCombiner : Combiner {
override fun processEvent(previousEvents: ArrayList<Event>?, event: Event): Event {
if (event.mKeyCode == KeyCode.SHIFT) return event
// previously we only used the combiner if codePoint > 0x1100 or codePoint == -1, but looks here it's not necessary
val event = HangulEventDecoder.decodeSoftwareKeyEvent(event)
if (Character.isWhitespace(event.mCodePoint)) {
val text = combiningStateFeedback
reset()

View file

@ -24,7 +24,8 @@ class HardwareKeyboardEventDecoder(val mDeviceId: Int) : HardwareEventDecoder {
// KeyEvent#getUnicodeChar() does not exactly returns a unicode char, but rather a value
// that includes both the unicode char in the lower 21 bits and flags in the upper bits,
// hence the name "codePointAndFlags". {@see KeyEvent#getUnicodeChar()} for more info.
val codePointAndFlags = keyEvent.unicodeChar
val codePointAndFlags = keyEvent.unicodeChar.takeIf { it != 0 }
?: Event.NOT_A_CODE_POINT // KeyEvent has 0 if no codePoint, but that's actually valid so we convert it to -1
// The keyCode is the abstraction used by the KeyEvent to represent different keys that
// do not necessarily map to a unicode character. This represents a physical key, like
// the key for 'A' or Space, but also Backspace or Ctrl or Caps Lock.
@ -48,6 +49,21 @@ class HardwareKeyboardEventDecoder(val mDeviceId: Int) : HardwareEventDecoder {
} else Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, metaState, null, isKeyRepeat)
// If not Enter, then this is just a regular keypress event for a normal character
// that can be committed right away, taking into account the current state.
} else Event.createNotHandledEvent()
} else if (isDpadDirection(keyCode)) {
Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, metaState, null, isKeyRepeat)
// } else if (KeyEvent.isModifierKey(keyCode)) {
// todo: we could synchronize meta state across HW and SW keyboard, but that's more work for little benefit (especially with shift & caps lock)
} else {
Event.notHandledEvent
}
}
companion object {
private fun isDpadDirection(keyCode: Int) = when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_DOWN_LEFT, KeyEvent.KEYCODE_DPAD_DOWN_RIGHT, KeyEvent.KEYCODE_DPAD_UP_RIGHT,
KeyEvent.KEYCODE_DPAD_UP_LEFT -> true
else -> false
}
}
}

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;
@ -517,11 +518,7 @@ public class Key implements Comparable<Key> {
}
public final boolean isModifier() {
return switch (mCode) {
case KeyCode.SHIFT, KeyCode.SYMBOL_ALPHA, KeyCode.ALPHA, KeyCode.SYMBOL, KeyCode.NUMPAD, KeyCode.CTRL,
KeyCode.ALT, KeyCode.FN, KeyCode.META -> true;
default -> false;
};
return KeyCode.INSTANCE.isModifier(mCode);
}
public final boolean isRepeatable() {
@ -919,7 +916,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 +930,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

@ -6,6 +6,8 @@
package helium314.keyboard.keyboard;
import android.view.KeyEvent;
import helium314.keyboard.latin.common.Constants;
import helium314.keyboard.latin.common.InputPointers;
@ -31,6 +33,12 @@ public interface KeyboardActionListener {
*/
void onReleaseKey(int primaryCode, boolean withSliding);
/** For handling hardware key presses. Returns whether the event was handled. */
boolean onKeyDown(int keyCode, KeyEvent keyEvent);
/** For handling hardware key presses. Returns whether the event was handled. */
boolean onKeyUp(int keyCode, KeyEvent keyEvent);
/**
* Send a key code to the listener.
*
@ -117,6 +125,10 @@ public interface KeyboardActionListener {
@Override
public void onReleaseKey(int primaryCode, boolean withSliding) {}
@Override
public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { return false; }
@Override
public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { return false; }
@Override
public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {}
@Override
public void onTextInput(String text) {}

View file

@ -1,21 +1,38 @@
package helium314.keyboard.keyboard
import android.text.InputType
import android.util.SparseArray
import android.view.KeyEvent
import android.view.inputmethod.InputMethodSubtype
import helium314.keyboard.event.Event
import helium314.keyboard.event.HangulEventDecoder
import helium314.keyboard.event.HardwareEventDecoder
import helium314.keyboard.event.HardwareKeyboardEventDecoder
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.EmojiAltPhysicalKeyDetector
import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.RichInputMethodManager
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.InputPointers
import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.common.combiningRange
import helium314.keyboard.latin.common.loopOverCodePoints
import helium314.keyboard.latin.common.loopOverCodePointsBackwards
import helium314.keyboard.latin.define.ProductionFlags
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 emojiAltPhysicalKeyDetector by lazy { EmojiAltPhysicalKeyDetector(latinIME.resources) }
// We expect to have only one decoder in almost all cases, hence the default capacity of 1.
// If it turns out we need several, it will get grown seamlessly.
private val hardwareEventDecoders: SparseArray<HardwareEventDecoder> = SparseArray(1)
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 +45,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()
@ -48,9 +71,62 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
keyboardSwitcher.onReleaseKey(primaryCode, withSliding, latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
}
override fun onKeyUp(keyCode: Int, keyEvent: KeyEvent): Boolean {
emojiAltPhysicalKeyDetector.onKeyUp(keyEvent)
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED)
return false
val keyIdentifier = keyEvent.deviceId.toLong() shl 32 + keyEvent.keyCode
return inputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)
}
override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean {
emojiAltPhysicalKeyDetector.onKeyDown(keyEvent)
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED)
return false
val event: Event
if (settings.current.mLocale.language == "ko") { // todo: this does not appear to be the right place
val subtype = keyboardSwitcher.keyboard?.mId?.mSubtype ?: RichInputMethodManager.getInstance().currentSubtype
event = HangulEventDecoder.decodeHardwareKeyEvent(subtype, keyEvent) {
getHardwareKeyEventDecoder(keyEvent.deviceId).decodeHardwareKey(keyEvent)
}
} else {
event = getHardwareKeyEventDecoder(keyEvent.deviceId).decodeHardwareKey(keyEvent)
}
if (event.isHandled) {
inputLogic.onCodeInput(
settings.current, event,
keyboardSwitcher.getKeyboardShiftMode(), // TODO: this is not necessarily correct for a hardware keyboard right now
keyboardSwitcher.getCurrentKeyboardScript(),
latinIME.mHandler
)
return true
}
return false
}
override fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean) {
when (primaryCode) {
KeyCode.TOGGLE_AUTOCORRECT -> return Settings.getInstance().toggleAutoCorrect()
KeyCode.TOGGLE_INCOGNITO_MODE -> return Settings.getInstance().toggleAlwaysIncognitoMode()
}
val mkv = keyboardSwitcher.mainKeyboardView
latinIME.onCodeInput(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
// checking if the character is a combining accent
val event = if (primaryCode in combiningRange) { // todo: should this be done later, maybe in inputLogic?
Event.createSoftwareDeadEvent(primaryCode, 0, metaState, mkv.getKeyX(x), mkv.getKeyY(y), null)
} else {
// todo:
// setting meta shift should only be done for arrow and similar cursor movement keys
// should only be enabled once it works more reliably (currently depends on app for some reason)
// if (mkv.keyboard?.mId?.isAlphabetShiftedManually == true)
// Event.createSoftwareKeypressEvent(primaryCode, metaState or KeyEvent.META_SHIFT_ON, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
// else Event.createSoftwareKeypressEvent(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
Event.createSoftwareKeypressEvent(primaryCode, metaState, mkv.getKeyX(x), mkv.getKeyY(y), isKeyRepeat)
}
latinIME.onEvent(event)
}
override fun onTextInput(text: String?) = latinIME.onTextInput(text)
@ -70,8 +146,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 +178,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)
}
@ -135,7 +216,7 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
private fun onLanguageSlide(steps: Int): Boolean {
if (abs(steps) < settings.current.mLanguageSwipeDistance) return false
val subtypes = RichInputMethodManager.getInstance().getMyEnabledInputMethodSubtypeList(false)
val subtypes = RichInputMethodManager.getInstance().getMyEnabledInputMethodSubtypes(true)
if (subtypes.size <= 1) { // only allow if we have more than one subtype
return false
}
@ -143,16 +224,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 +255,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 +266,70 @@ 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)
}
private fun getHardwareKeyEventDecoder(deviceId: Int): HardwareEventDecoder {
hardwareEventDecoders.get(deviceId)?.let { return it }
// TODO: create the decoder according to the specification
val newDecoder = HardwareKeyboardEventDecoder(deviceId)
hardwareEventDecoders.put(deviceId, newDecoder)
return newDecoder
}
}

View file

@ -184,6 +184,11 @@ public final class KeyboardId {
|| mElementId == ELEMENT_ALPHABET_AUTOMATIC_SHIFTED || mElementId == ELEMENT_ALPHABET_MANUAL_SHIFTED;
}
public boolean isAlphabetShiftedManually() {
return mElementId == ELEMENT_ALPHABET_SHIFT_LOCKED || mElementId == ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
|| mElementId == ELEMENT_ALPHABET_MANUAL_SHIFTED;
}
public boolean isNumberLayout() {
return mElementId == ELEMENT_NUMBER || mElementId == ELEMENT_NUMPAD
|| mElementId == ELEMENT_PHONE || mElementId == ELEMENT_PHONE_SYMBOLS;

View file

@ -6,12 +6,11 @@
package helium314.keyboard.keyboard;
import android.app.KeyguardManager;
import android.content.Context;
import android.os.Build;
import android.text.InputType;
import android.view.inputmethod.EditorInfo;
import helium314.keyboard.compat.IsLockedCompatKt;
import helium314.keyboard.keyboard.internal.KeyboardBuilder;
import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
import helium314.keyboard.keyboard.internal.KeyboardParams;
@ -96,7 +95,7 @@ public final class KeyboardLayoutSet {
public static void onSystemLocaleChanged() {
clearKeyboardCache();
LocaleKeyboardInfosKt.clearCache();
SubtypeLocaleUtils.clearDisplayNameCache();
SubtypeLocaleUtils.clearSubtypeDisplayNameCache();
}
public static void onKeyboardThemeChanged() {
@ -207,14 +206,8 @@ public final class KeyboardLayoutSet {
params.mEditorInfo = editorInfo;
params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
// When the device is still locked, features like showing the IME setting app need to
// be locked down.
final KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
params.mDeviceLocked = km.isDeviceLocked();
} else {
params.mDeviceLocked = km.isKeyguardLocked();
}
// When the device is still locked, features like showing the IME setting app need to be locked down.
params.mDeviceLocked = IsLockedCompatKt.isDeviceLocked(context);
}
public static KeyboardLayoutSet buildEmojiClipBottomRow(final Context context, @Nullable final EditorInfo ei) {

View file

@ -20,6 +20,7 @@ import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.FrameLayout;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;
@ -49,8 +50,8 @@ import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.RecapitalizeStatus;
import helium314.keyboard.latin.utils.ResourceUtils;
import helium314.keyboard.latin.utils.ScriptUtils;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
import helium314.keyboard.latin.utils.SubtypeUtilsAdditional;
import helium314.keyboard.latin.utils.ToolbarMode;
public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
private static final String TAG = KeyboardSwitcher.class.getSimpleName();
@ -64,6 +65,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
private LinearLayout mClipboardStripView;
private HorizontalScrollView mClipboardStripScrollView;
private SuggestionStripView mSuggestionStripView;
private FrameLayout mStripContainer;
private ClipboardHistoryView mClipboardHistoryView;
private TextView mFakeToastView;
private LatinIME mLatinIME;
@ -136,7 +138,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
return false;
}
public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
private void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues,
final int currentAutoCapsState, final int currentRecapitalizeState) {
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
mThemeContext, editorInfo);
@ -157,10 +159,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 +170,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());
}
}
}
@ -308,6 +309,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
@NonNull final SettingsValues settingsValues,
@NonNull final KeyboardSwitchState toggleState) {
final int visibility = isImeSuppressedByHardwareKeyboard(settingsValues, toggleState) ? View.GONE : View.VISIBLE;
final int stripVisibility = settingsValues.mToolbarMode == ToolbarMode.HIDDEN ? View.GONE : View.VISIBLE;
mStripContainer.setVisibility(stripVisibility);
PointerTracker.switchTo(mKeyboardView);
mKeyboardView.setVisibility(visibility);
// The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}.
@ -318,7 +321,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mEmojiPalettesView.stopEmojiPalettes();
mEmojiTabStripView.setVisibility(View.GONE);
mClipboardStripScrollView.setVisibility(View.GONE);
mSuggestionStripView.setVisibility(View.VISIBLE);
mSuggestionStripView.setVisibility(stripVisibility);
mClipboardHistoryView.setVisibility(View.GONE);
mClipboardHistoryView.stopClipboardHistory();
}
@ -335,6 +338,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
// @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets)
mKeyboardView.setVisibility(View.GONE);
mSuggestionStripView.setVisibility(View.GONE);
mStripContainer.setVisibility(getSecondaryStripVisibility());
mClipboardStripScrollView.setVisibility(View.GONE);
mEmojiTabStripView.setVisibility(View.VISIBLE);
mClipboardHistoryView.setVisibility(View.GONE);
@ -356,6 +360,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mKeyboardView.setVisibility(View.GONE);
mEmojiTabStripView.setVisibility(View.GONE);
mSuggestionStripView.setVisibility(View.GONE);
mStripContainer.setVisibility(getSecondaryStripVisibility());
mClipboardStripScrollView.post(() -> mClipboardStripScrollView.fullScroll(HorizontalScrollView.FOCUS_RIGHT));
mClipboardStripScrollView.setVisibility(View.VISIBLE);
mEmojiPalettesView.setVisibility(View.GONE);
@ -477,10 +482,13 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setOneHandedModeEnabled(boolean enabled) {
if (mKeyboardViewWrapper.getOneHandedModeEnabled() == enabled) {
setOneHandedModeEnabled(enabled, false);
}
public void setOneHandedModeEnabled(boolean enabled, boolean force) {
if (!force && mKeyboardViewWrapper.getOneHandedModeEnabled() == enabled) {
return;
}
mEmojiPalettesView.clearKeyboardCache();
final Settings settings = Settings.getInstance();
mKeyboardViewWrapper.setOneHandedModeEnabled(enabled);
mKeyboardViewWrapper.setOneHandedGravity(settings.getCurrent().mOneHandedModeGravity);
@ -511,13 +519,20 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
!settings.getCurrent().mIsSplitKeyboardEnabled,
mCurrentOrientation == Configuration.ORIENTATION_LANDSCAPE
);
setOneHandedModeEnabled(settings.getCurrent().mOneHandedModeEnabled, true);
reloadKeyboard();
}
public void reloadKeyboard() {
if (mCurrentInputView != null)
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
if (mCurrentInputView == null)
return;
mEmojiPalettesView.clearKeyboardCache();
reloadMainKeyboard();
}
public void reloadMainKeyboard() {
loadKeyboard(mLatinIME.getCurrentInputEditorInfo(), Settings.getValues(),
mLatinIME.getCurrentAutoCapsState(), mLatinIME.getCurrentRecapitalizeState());
}
/**
@ -539,6 +554,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
}
private static int getSecondaryStripVisibility() {
return Settings.getValues().mSecondaryStripVisible? View.VISIBLE : View.GONE;
}
// Displays a toast-like message with the provided text for a specified duration.
private void showFakeToast(final String text, final int timeMillis) {
if (mFakeToastView.getVisibility() == View.VISIBLE) return;
@ -582,7 +601,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
if (mKeyboardView == null || !mKeyboardView.isShown()) {
return false;
}
int activeKeyboardId = mKeyboardView.getKeyboard().mId.mElementId;
final Keyboard keyboard = mKeyboardView.getKeyboard();
if (keyboard == null) // may happen when using hardware keyboard
return false;
int activeKeyboardId = keyboard.mId.mElementId;
for (int keyboardId : keyboardIds) {
if (activeKeyboardId == keyboardId) {
return true;
@ -606,6 +628,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
return mKeyboardView.isShowingPopupKeysPanel();
}
public boolean isShowingStripContainer() {
return mStripContainer.isShown();
}
public View getVisibleKeyboardView() {
if (isShowingEmojiPalettes()) {
return mEmojiPalettesView;
@ -631,6 +657,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
return mKeyboardView;
}
public FrameLayout getStripContainer() { return mStripContainer; }
public void deallocateMemory() {
if (mKeyboardView != null) {
mKeyboardView.cancelAllOngoingEvents();
@ -644,6 +672,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) {
@ -678,6 +712,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mClipboardStripView = mCurrentInputView.findViewById(R.id.clipboard_strip);
mClipboardStripScrollView = mCurrentInputView.findViewById(R.id.clipboard_strip_scroll_view);
mSuggestionStripView = mCurrentInputView.findViewById(R.id.suggestion_strip_view);
mStripContainer = mCurrentInputView.findViewById(R.id.strip_container);
prefs.registerOnSharedPreferenceChangeListener(mSuggestionStripView);
prefs.registerOnSharedPreferenceChangeListener(mClipboardHistoryView);

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,10 +35,10 @@ 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;
import helium314.keyboard.latin.suggestions.MoreSuggestionsView;
import helium314.keyboard.latin.utils.TypefaceUtils;
import java.util.HashSet;
@ -108,7 +109,7 @@ public class KeyboardView extends View {
final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
if (this instanceof PopupSuggestionsView)
if (this instanceof MoreSuggestionsView)
mKeyBackground = mColors.selectAndColorDrawable(keyboardViewAttr, ColorType.MORE_SUGGESTIONS_WORD_BACKGROUND);
else if (this instanceof PopupKeysKeyboardView)
mKeyBackground = mColors.selectAndColorDrawable(keyboardViewAttr, ColorType.POPUP_KEYS_BACKGROUND);
@ -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;
@ -359,25 +360,21 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
public void onKeyPressed(@NonNull final Key key, final boolean withPreview) {
key.onPressed();
invalidateKey(key);
if (withPreview && !key.noKeyPreview()) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
mKeyPreviewDrawParams.setVisibleOffset(-keyboard.mVerticalGap);
if (withPreview && !key.noKeyPreview() && mKeyPreviewDrawParams.isPopupEnabled()) {
showKeyPreview(key);
}
}
private void showKeyPreview(@NonNull final Key key) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {
return;
}
final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
if (!previewParams.isPopupEnabled()) {
previewParams.setVisibleOffset(-keyboard.mVerticalGap);
return;
}
locatePreviewPlacerView();
getLocationInWindow(mOriginCoords);
mKeyPreviewChoreographer.placeAndShowKeyPreview(key, keyboard.mIconsSet, getKeyDrawParams(),
mKeyPreviewChoreographer.placeAndShowKeyPreview(key, getKeyboard().mIconsSet, getKeyDrawParams(),
KeyboardSwitcher.getInstance().getWrapperView().getWidth(), mOriginCoords, mDrawingPreviewPlacerView);
}
@ -505,7 +502,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

@ -12,15 +12,15 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import helium314.keyboard.accessibility.AccessibilityUtils;
import helium314.keyboard.accessibility.PopupKeysKeyboardAccessibilityDelegate;
import helium314.keyboard.keyboard.emoji.OnKeyEventListener;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.R;
@ -39,7 +39,7 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
protected final KeyDetector mKeyDetector;
private Controller mController = EMPTY_CONTROLLER;
protected KeyboardActionListener mListener;
protected OnKeyEventListener mKeyEventListener;
protected EmojiViewCallback mEmojiViewCallback;
private int mOriginX;
private int mOriginY;
private Key mCurrentKey;
@ -122,7 +122,7 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final KeyboardActionListener listener) {
mListener = listener;
mKeyEventListener = null;
mEmojiViewCallback = null;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@ -131,9 +131,9 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
*/
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final OnKeyEventListener listener) {
final int pointX, final int pointY, final EmojiViewCallback emojiViewCallback) {
mListener = null;
mKeyEventListener = listener;
mEmojiViewCallback = emojiViewCallback;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@ -157,6 +157,9 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
mOriginX = x + container.getPaddingLeft();
mOriginY = y + container.getPaddingTop();
var center = panelX + getMeasuredWidth() / 2;
// This is needed for cases where there's also a long text popup above this keyboard
controller.setLayoutGravity(center < pointX? Gravity.RIGHT : center > pointX? Gravity.LEFT : Gravity.CENTER_HORIZONTAL);
controller.onShowPopupKeysPanel(this);
final PopupKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
if (accessibilityDelegate != null
@ -222,8 +225,8 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
false /* isKeyRepeat */);
}
}
} else if (mKeyEventListener != null) {
mKeyEventListener.onReleaseKey(key);
} else if (mEmojiViewCallback != null) {
mEmojiViewCallback.onReleaseKey(key);
}
}
@ -314,28 +317,4 @@ public class PopupKeysKeyboardView extends KeyboardView implements PopupKeysPane
}
return super.onHoverEvent(event);
}
private View getContainerView() {
return (View)getParent();
}
@Override
public void showInParent(final ViewGroup parentView) {
removeFromParent();
parentView.addView(getContainerView());
}
@Override
public void removeFromParent() {
final View containerView = getContainerView();
final ViewGroup currentParent = (ViewGroup)containerView.getParent();
if (currentParent != null) {
currentParent.removeView(containerView);
}
}
@Override
public boolean isShowingInParent() {
return (getContainerView().getParent() != null);
}
}

View file

@ -8,10 +8,17 @@ package helium314.keyboard.keyboard;
import android.view.View;
import android.view.ViewGroup;
import helium314.keyboard.keyboard.emoji.OnKeyEventListener;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
public interface PopupKeysPanel {
interface Controller {
/**
* Set the layout gravity.
* @param layoutGravity requested by the popup
*/
default void setLayoutGravity(int layoutGravity) {
}
/**
* Add the {@link PopupKeysPanel} to the target view.
* @param panel the panel to be shown.
@ -59,19 +66,18 @@ public interface PopupKeysPanel {
* Initializes the layout and event handling of this {@link PopupKeysPanel} and calls the
* controller's onShowPopupKeysPanel to add the panel's container view.
* Same as {@link PopupKeysPanel#showPopupKeysPanel(View, Controller, int, int, KeyboardActionListener)},
* but with a {@link OnKeyEventListener}.
* but with a {@link EmojiViewCallback}.
*
* @param parentView the parent view of this {@link PopupKeysPanel}
* @param controller the controller that can dismiss this {@link PopupKeysPanel}
* @param pointX x coordinate of this {@link PopupKeysPanel}
* @param pointY y coordinate of this {@link PopupKeysPanel}
* @param listener the listener that will receive keyboard action from this
* {@link PopupKeysPanel}.
* @param emojiViewCallback to receive keyboard actions from this {@link PopupKeysPanel}.
*/
// TODO: Currently the PopupKeysPanel is inside a container view that is added to the parent.
// Consider the simpler approach of placing the PopupKeysPanel itself into the parent view.
void showPopupKeysPanel(View parentView, Controller controller, int pointX,
int pointY, OnKeyEventListener listener);
int pointY, EmojiViewCallback emojiViewCallback);
/**
* Dismisses the popup keys panel and calls the controller's onDismissPopupKeysPanel to remove
@ -127,20 +133,35 @@ public interface PopupKeysPanel {
*/
int translateY(int y);
default View getContainerView() {
return (View) ((View) this).getParent();
}
/**
* Show this {@link PopupKeysPanel} in the parent view.
*
* @param parentView the {@link ViewGroup} that hosts this {@link PopupKeysPanel}.
*/
void showInParent(ViewGroup parentView);
default void showInParent(ViewGroup parentView) {
removeFromParent();
parentView.addView(getContainerView());
}
/**
* Remove this {@link PopupKeysPanel} from the parent view.
*/
void removeFromParent();
default void removeFromParent() {
final View containerView = getContainerView();
final ViewGroup currentParent = (ViewGroup)containerView.getParent();
if (currentParent != null) {
currentParent.removeView(containerView);
}
}
/**
* Return whether the panel is currently being shown.
*/
boolean isShowingInParent();
default boolean isShowingInParent() {
return getContainerView().getParent() != null;
}
}

View file

@ -0,0 +1,121 @@
/*
* Copyright (C) 2011 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/
package helium314.keyboard.keyboard;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;
import helium314.keyboard.keyboard.emoji.EmojiViewCallback;
import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.CoordinateUtils;
import helium314.keyboard.latin.settings.Settings;
/**
* A view that displays popup text.
*/
public class PopupTextView extends TextView implements PopupKeysPanel {
private final int[] mCoordinates = CoordinateUtils.newInstance();
private final Typeface mTypeface;
private Controller mController = EMPTY_CONTROLLER;
private int mOriginX;
private int mOriginY;
private Key mKey;
private EmojiViewCallback mEmojiViewCallback;
public PopupTextView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.popupKeysKeyboardViewStyle);
}
public PopupTextView(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
mTypeface = Settings.getInstance().getCustomTypeface();
}
public void setKeyDrawParams(Key key, KeyDrawParams drawParams) {
mKey = key;
Settings.getValues().mColors.setBackground(this, ColorType.KEY_PREVIEW_BACKGROUND);
setTextColor(drawParams.mPreviewTextColor);
setTextSize(TypedValue.COMPLEX_UNIT_PX, key.selectHintTextSize(drawParams) << 1);
setTypeface(mTypeface == null ? key.selectTypeface(drawParams) : mTypeface);
}
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final KeyboardActionListener listener) {
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
@Override
public void showPopupKeysPanel(final View parentView, final Controller controller,
final int pointX, final int pointY, final EmojiViewCallback emojiViewCallback) {
mEmojiViewCallback = emojiViewCallback;
showPopupKeysPanelInternal(parentView, controller, pointX, pointY);
}
private void showPopupKeysPanelInternal(final View parentView, final Controller controller,
final int pointX, final int pointY) {
mController = controller;
final View container = getContainerView();
// The coordinates of panel's left-top corner in parentView's coordinate system.
// We need to consider background drawable paddings.
final int x = pointX - getMeasuredWidth() / 2 - container.getPaddingLeft() - getPaddingLeft();
final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom()
+ getPaddingBottom();
parentView.getLocationInWindow(mCoordinates);
// Ensure the horizontal position of the panel does not extend past the parentView edges.
final int maxX = parentView.getMeasuredWidth() - container.getMeasuredWidth();
final int panelX = Math.max(0, Math.min(maxX, x)) + CoordinateUtils.x(mCoordinates);
final int panelY = y + CoordinateUtils.y(mCoordinates);
container.setX(panelX);
container.setY(panelY);
mOriginX = x + container.getPaddingLeft();
mOriginY = y + container.getPaddingTop();
controller.setLayoutGravity(Gravity.NO_GRAVITY);
controller.onShowPopupKeysPanel(this);
}
@Override
public void onDownEvent(final int x, final int y, final int pointerId, final long eventTime) {
}
@Override
public void onMoveEvent(final int x, final int y, final int pointerId, final long eventTime) {
}
@Override
public void onUpEvent(final int x, final int y, final int pointerId, final long eventTime) {
mEmojiViewCallback.onReleaseKey(mKey);
}
@Override
public void dismissPopupKeysPanel() {
if (!isShowingInParent()) {
return;
}
mController.onDismissPopupKeysPanel();
}
@Override
public int translateX(final int x) {
return x - mOriginX;
}
@Override
public int translateY(final int y) {
return y - mOriginY;
}
}

View file

@ -21,7 +21,6 @@ import helium314.keyboard.keyboard.MainKeyboardView
import helium314.keyboard.keyboard.PointerTracker
import helium314.keyboard.keyboard.internal.KeyDrawParams
import helium314.keyboard.keyboard.internal.KeyVisualAttributes
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.ClipboardHistoryManager
import helium314.keyboard.latin.R
@ -49,15 +48,14 @@ class ClipboardHistoryView @JvmOverloads constructor(
private val clipboardLayoutParams = ClipboardLayoutParams(context)
private val pinIconId: Int
private val keyBackgroundId: Int
private var initialized = false
private lateinit var clipboardRecyclerView: ClipboardHistoryRecyclerView
private lateinit var placeholderView: TextView
private val toolbarKeys = mutableListOf<ImageButton>()
private lateinit var clipboardAdapter: ClipboardAdapter
var keyboardActionListener: KeyboardActionListener? = null
var clipboardHistoryManager: ClipboardHistoryManager? = null
lateinit var keyboardActionListener: KeyboardActionListener
private var clipboardHistoryManager: ClipboardHistoryManager? = null
init {
val clipboardViewAttr = context.obtainStyledAttributes(attrs,
@ -67,10 +65,11 @@ class ClipboardHistoryView @JvmOverloads constructor(
val keyboardViewAttr = context.obtainStyledAttributes(attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView)
keyBackgroundId = keyboardViewAttr.getResourceId(R.styleable.KeyboardView_keyBackground, 0)
keyboardViewAttr.recycle()
val keyboardAttr = context.obtainStyledAttributes(attrs, R.styleable.Keyboard, defStyle, R.style.SuggestionStripView)
getEnabledClipboardToolbarKeys(context.prefs())
.forEach { toolbarKeys.add(createToolbarKey(context, KeyboardIconsSet.instance, it)) }
keyboardAttr.recycle()
if (Settings.getValues().mSecondaryStripVisible) {
getEnabledClipboardToolbarKeys(context.prefs())
.forEach { toolbarKeys.add(createToolbarKey(context, it)) }
}
fitsSystemWindows = true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@ -78,13 +77,13 @@ class ClipboardHistoryView @JvmOverloads constructor(
val res = context.resources
// The main keyboard expands to the entire this {@link KeyboardView}.
val width = ResourceUtils.getKeyboardWidth(context, Settings.getValues()) + paddingLeft + paddingRight
val height = ResourceUtils.getKeyboardHeight(res, Settings.getValues()) + paddingTop + paddingBottom
val height = ResourceUtils.getSecondaryKeyboardHeight(res, Settings.getValues()) + paddingTop + paddingBottom
setMeasuredDimension(width, height)
}
@SuppressLint("ClickableViewAccessibility")
private fun initialize() { // needs to be delayed for access to ClipboardStrip, which is not a child of this view
if (initialized) return
if (this::clipboardAdapter.isInitialized) return
val colors = Settings.getValues().mColors
clipboardAdapter = ClipboardAdapter(clipboardLayoutParams, this).apply {
itemBackgroundId = keyBackgroundId
@ -107,7 +106,6 @@ class ClipboardHistoryView @JvmOverloads constructor(
colors.setColor(it, ColorType.TOOL_BAR_KEY)
colors.setBackground(it, ColorType.STRIP_BACKGROUND)
}
initialized = true
}
private fun setupClipKey(params: KeyDrawParams) {
@ -188,7 +186,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
}
fun stopClipboardHistory() {
if (!initialized) return
if (!this::clipboardAdapter.isInitialized) return
clipboardRecyclerView.adapter = null
clipboardHistoryManager?.setHistoryChangeListener(null)
clipboardHistoryManager = null
@ -200,7 +198,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
if (tag is ToolbarKey) {
val code = getCodeForToolbarKey(tag)
if (code != KeyCode.UNSPECIFIED) {
keyboardActionListener?.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
keyboardActionListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
return
}
}
@ -211,7 +209,7 @@ class ClipboardHistoryView @JvmOverloads constructor(
if (tag is ToolbarKey) {
val longClickCode = getCodeForToolbarKeyLongClick(tag)
if (longClickCode != KeyCode.UNSPECIFIED) {
keyboardActionListener?.onCodeInput(
keyboardActionListener.onCodeInput(
longClickCode,
Constants.NOT_A_COORDINATE,
Constants.NOT_A_COORDINATE,
@ -224,15 +222,15 @@ class ClipboardHistoryView @JvmOverloads constructor(
}
override fun onKeyDown(clipId: Long) {
keyboardActionListener?.onPressKey(KeyCode.NOT_SPECIFIED, 0, true)
keyboardActionListener.onPressKey(KeyCode.NOT_SPECIFIED, 0, true)
}
override fun onKeyUp(clipId: Long) {
val clipContent = clipboardHistoryManager?.getHistoryEntryContent(clipId)
keyboardActionListener?.onTextInput(clipContent?.content.toString())
keyboardActionListener?.onReleaseKey(KeyCode.NOT_SPECIFIED, false)
keyboardActionListener.onTextInput(clipContent?.content.toString())
keyboardActionListener.onReleaseKey(KeyCode.NOT_SPECIFIED, false)
if (Settings.getValues().mAlphaAfterClipHistoryEntry)
keyboardActionListener?.onCodeInput(KeyCode.ALPHA, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
keyboardActionListener.onCodeInput(KeyCode.ALPHA, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
}
override fun onClipboardHistoryEntryAdded(at: Int) {

View file

@ -3,7 +3,6 @@
package helium314.keyboard.keyboard.clipboard
import android.content.Context
import android.content.res.Resources
import android.view.View
import android.widget.FrameLayout
import androidx.recyclerview.widget.RecyclerView
@ -22,7 +21,7 @@ class ClipboardLayoutParams(ctx: Context) {
init {
val res = ctx.resources
val sv = Settings.getValues()
val defaultKeyboardHeight = ResourceUtils.getKeyboardHeight(res, sv)
val defaultKeyboardHeight = ResourceUtils.getSecondaryKeyboardHeight(res, sv)
val defaultKeyboardWidth = ResourceUtils.getKeyboardWidth(ctx, sv)
if (sv.mNarrowKeyGaps) {

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);
@ -113,12 +112,12 @@ final class DynamicGridKeyboard extends Keyboard {
}
public int getDynamicOccupiedHeight() {
final int row = (mGridKeys.size() - 1) / mColumnsNum + 1;
final int row = (mGridKeys.size() - 1) / getOccupiedColumnCount() + 1;
return row * mVerticalStep;
}
public int getColumnsCount() {
return mColumnsNum;
public int getOccupiedColumnCount() {
return mColumnsNum - mEmptyColumnIndices.size();
}
public void addPendingKey(final Key usedKey) {
@ -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;
}
@ -338,7 +324,7 @@ final class EmojiCategory {
final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
0, 0, ResourceUtils.getKeyboardWidth(mContext, Settings.getValues()));
return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getColumnsCount();
return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getOccupiedColumnCount();
}
private static final Comparator<Key> EMOJI_KEY_COMPARATOR = (lhs, rhs) -> {

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
@ -22,7 +22,7 @@ internal class EmojiLayoutParams(res: Resources) {
init {
val sv = Settings.getValues()
val defaultKeyboardHeight = ResourceUtils.getKeyboardHeight(res, sv)
val defaultKeyboardHeight = ResourceUtils.getSecondaryKeyboardHeight(res, sv)
val keyVerticalGap = if (sv.mNarrowKeyGaps) {
res.getFraction(R.fraction.config_key_vertical_gap_holo_narrow,
@ -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

@ -13,6 +13,9 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.LinearLayout;
import helium314.keyboard.keyboard.PopupTextView;
import helium314.keyboard.latin.utils.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@ -53,14 +56,18 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
private static final long KEY_PRESS_DELAY_TIME = 250; // msec
private static final long KEY_RELEASE_DELAY_TIME = 30; // msec
private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() {
private static final EmojiViewCallback EMPTY_EMOJI_VIEW_CALLBACK = new EmojiViewCallback() {
@Override
public void onPressKey(final Key key) {}
@Override
public void onReleaseKey(final Key key) {}
@Override
public String getDescription(String emoji) {
return null;
}
};
private OnKeyEventListener mListener = EMPTY_LISTENER;
private EmojiViewCallback mEmojiViewCallback = EMPTY_EMOJI_VIEW_CALLBACK;
private final KeyDetector mKeyDetector = new KeyDetector();
private KeyboardAccessibilityDelegate<EmojiPageKeyboardView> mAccessibilityDelegate;
@ -74,6 +81,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
// More keys keyboard
private final View mPopupKeysKeyboardContainer;
private final PopupTextView mDescriptionView;
private final PopupKeysKeyboardView mPopupKeysKeyboardView;
private final WeakHashMap<Key, Keyboard> mPopupKeysKeyboardCache = new WeakHashMap<>();
private final boolean mConfigShowPopupKeysKeyboardAtTouchedPoint;
private final ViewGroup mPopupKeysPlacerView;
@ -102,6 +111,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
final LayoutInflater inflater = LayoutInflater.from(getContext());
mPopupKeysKeyboardContainer = inflater.inflate(popupKeysKeyboardLayoutId, null);
mDescriptionView = mPopupKeysKeyboardContainer.findViewById(R.id.description_view);
mPopupKeysKeyboardView = mPopupKeysKeyboardContainer.findViewById(R.id.popup_keys_keyboard_view);
}
@Override
@ -146,8 +157,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
}
}
public void setOnKeyEventListener(final OnKeyEventListener listener) {
mListener = listener;
public void setEmojiViewCallback(final EmojiViewCallback emojiViewCallback) {
mEmojiViewCallback = emojiViewCallback;
}
/**
@ -169,7 +180,8 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
}
@Nullable
public PopupKeysPanel showPopupKeysKeyboard(@NonNull final Key key, final int lastX, final int lastY) {
private PopupKeysPanel showPopupKeysKeyboard(@NonNull final Key key) {
mPopupKeysKeyboardView.setVisibility(GONE);
final PopupKeySpec[] popupKeys = key.getPopupKeys();
if (popupKeys == null) {
return null;
@ -182,21 +194,9 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
mPopupKeysKeyboardCache.put(key, popupKeysKeyboard);
}
final View container = mPopupKeysKeyboardContainer;
final PopupKeysKeyboardView popupKeysKeyboardView = container.findViewById(R.id.popup_keys_keyboard_view);
popupKeysKeyboardView.setKeyboard(popupKeysKeyboard);
container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int[] lastCoords = CoordinateUtils.newCoordinateArray(1, lastX, lastY);
// The popup keys keyboard is usually horizontally aligned with the center of the parent key.
// If showPopupKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
// keys keyboard is placed at the touch point of the parent key.
final int pointX = mConfigShowPopupKeysKeyboardAtTouchedPoint
? CoordinateUtils.x(lastCoords)
: key.getX() + key.getWidth() / 2;
final int pointY = key.getY();
popupKeysKeyboardView.showPopupKeysPanel(this, this, pointX, pointY, mListener);
return popupKeysKeyboardView;
mPopupKeysKeyboardView.setKeyboard(popupKeysKeyboard);
mPopupKeysKeyboardView.setVisibility(VISIBLE);
return mPopupKeysKeyboardView;
}
private void dismissPopupKeysPanel() {
@ -209,6 +209,17 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
return mPopupKeysPanel != null;
}
@Override
public void setLayoutGravity(int layoutGravity) {
var layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = mDescriptionView.getMeasuredWidth() > mPopupKeysKeyboardView.getMeasuredWidth()?
layoutGravity : Gravity.CENTER_HORIZONTAL;
mPopupKeysKeyboardContainer.setLayoutParams(layoutParams);
mDescriptionView.setLayoutParams(layoutParams);
mPopupKeysKeyboardView.setLayoutParams(layoutParams);
}
@Override
public void onShowPopupKeysPanel(final PopupKeysPanel panel) {
// install placer view only when needed instead of when this
@ -290,9 +301,11 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
return;
}
var descriptionPanel = showDescription(key);
final PopupKeysPanel popupKeysPanel = showPopupKeysKeyboard(key);
final int x = mLastX;
final int y = mLastY;
final PopupKeysPanel popupKeysPanel = showPopupKeysKeyboard(key, x, y);
if (popupKeysPanel != null) {
final int translatedX = popupKeysPanel.translateX(x);
final int translatedY = popupKeysPanel.translateY(y);
@ -301,6 +314,34 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
// want any scroll to append during this entire input.
disallowParentInterceptTouchEvent(true);
}
if (popupKeysPanel != null || descriptionPanel != null) {
mPopupKeysKeyboardContainer.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int[] lastCoords = CoordinateUtils.newCoordinateArray(1, x, y);
// The popup keys keyboard is usually horizontally aligned with the center of the parent key.
// If showPopupKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
// keys keyboard is placed at the touch point of the parent key.
final int pointX = mConfigShowPopupKeysKeyboardAtTouchedPoint
? CoordinateUtils.x(lastCoords)
: key.getX() + key.getWidth() / 2;
final int pointY = key.getY() - getKeyboard().mVerticalGap;
(popupKeysPanel != null? popupKeysPanel : descriptionPanel)
.showPopupKeysPanel(this, this, pointX, pointY, mEmojiViewCallback);
}
}
private PopupKeysPanel showDescription(Key key) {
mDescriptionView.setVisibility(GONE);
var description = mEmojiViewCallback.getDescription(key.getLabel());
if (description == null) {
return null;
}
mDescriptionView.setText(description);
mDescriptionView.setKeyDrawParams(key, getKeyDrawParams());
mDescriptionView.setVisibility(VISIBLE);
return mDescriptionView;
}
private void registerPress(final Key key) {
@ -318,7 +359,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
releasedKey.onReleased();
invalidateKey(releasedKey);
if (withKeyRegistering) {
mListener.onReleaseKey(releasedKey);
mEmojiViewCallback.onReleaseKey(releasedKey);
}
}
@ -326,7 +367,7 @@ public final class EmojiPageKeyboardView extends KeyboardView implements
mPendingKeyDown = null;
pressedKey.onPressed();
invalidateKey(pressedKey);
mListener.onPressKey(pressedKey);
mEmojiViewCallback.onPressKey(pressedKey);
}
public void releaseCurrentKey(final boolean withKeyRegistering) {

View file

@ -7,161 +7,37 @@
package helium314.keyboard.keyboard.emoji;
import helium314.keyboard.latin.utils.Log;
import android.util.SparseArray;
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 OnKeyEventListener mListener;
private final DynamicGridKeyboard mRecentsKeyboard;
private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews = new SparseArray<>();
private final int mCategoryId;
private final EmojiViewCallback mEmojiViewCallback;
private final EmojiCategory mEmojiCategory;
private int mActivePosition = 0;
public EmojiPalettesAdapter(final EmojiCategory emojiCategory,
final OnKeyEventListener listener) {
public EmojiPalettesAdapter(final EmojiCategory emojiCategory, int categoryId, final EmojiViewCallback emojiViewCallback) {
mEmojiCategory = emojiCategory;
mListener = listener;
mRecentsKeyboard = mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0);
mCategoryId = categoryId;
mEmojiViewCallback = emojiViewCallback;
}
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);*/
keyboardView.setEmojiViewCallback(mEmojiViewCallback);
return new ViewHolder(keyboardView);
}
@ -170,33 +46,21 @@ 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);
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;*/
final Keyboard keyboard =
mEmojiCategory.getKeyboardFromAdapterPosition(mCategoryId, position);
holder.getKeyboardView().setKeyboard(keyboard);
}
@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 +74,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;
@ -33,16 +38,18 @@ import helium314.keyboard.keyboard.internal.KeyDrawParams;
import helium314.keyboard.keyboard.internal.KeyVisualAttributes;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.AudioAndHapticFeedbackManager;
import helium314.keyboard.latin.DictionaryFactory;
import helium314.keyboard.latin.R;
import helium314.keyboard.latin.RichInputMethodManager;
import helium314.keyboard.latin.RichInputMethodSubtype;
import helium314.keyboard.latin.SingleDictionaryFacilitator;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.Colors;
import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.settings.SettingsValues;
import helium314.keyboard.latin.utils.DictionaryInfoUtils;
import helium314.keyboard.latin.utils.ResourceUtils;
import org.jetbrains.annotations.NotNull;
import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
/**
@ -57,27 +64,135 @@ import static helium314.keyboard.latin.common.Constants.NOT_A_COORDINATE;
* Because of the above reasons, this class doesn't extend {@link KeyboardView}.
*/
public final class EmojiPalettesView extends LinearLayout
implements View.OnClickListener, OnKeyEventListener {
implements View.OnClickListener, EmojiViewCallback {
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 final 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 static SingleDictionaryFacilitator sDictionaryFacilitator;
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 +211,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
@ -115,19 +222,12 @@ public final class EmojiPalettesView extends LinearLayout
// The main keyboard expands to the entire this {@link KeyboardView}.
final int width = ResourceUtils.getKeyboardWidth(getContext(), Settings.getValues())
+ getPaddingLeft() + getPaddingRight();
final int height = ResourceUtils.getKeyboardHeight(res, Settings.getValues())
final int height = ResourceUtils.getSecondaryKeyboardHeight(res, Settings.getValues())
+ getPaddingTop() + getPaddingBottom();
mEmojiCategoryPageIndicatorView.mWidth = width;
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);
@ -146,63 +246,18 @@ public final class EmojiPalettesView extends LinearLayout
if (initialized) return;
mEmojiCategory.initialize();
mTabStrip = (LinearLayout) KeyboardSwitcher.getInstance().getEmojiTabStrip();
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.
if (Settings.getValues().mSecondaryStripVisible) {
for (final EmojiCategory.CategoryProperties properties : mEmojiCategory.getShownCategories()) {
addTab(mTabStrip, properties.mCategoryId);
}
}
@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,15 +274,14 @@ 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();
}
}
}
/**
* Called from {@link EmojiPageKeyboardView} through
* {@link helium314.keyboard.keyboard.emoji.OnKeyEventListener}
* Called from {@link EmojiPageKeyboardView} through {@link EmojiViewCallback}
* interface to handle touch events from non-View-based elements such as Emoji buttons.
*/
@Override
@ -237,14 +291,13 @@ public final class EmojiPalettesView extends LinearLayout
}
/**
* Called from {@link EmojiPageKeyboardView} through
* {@link helium314.keyboard.keyboard.emoji.OnKeyEventListener}
* Called from {@link EmojiPageKeyboardView} through {@link EmojiViewCallback}
* interface to handle touch events from non-View-based elements such as Emoji buttons.
* This may be called without any prior call to {@link OnKeyEventListener#onPressKey(Key)}.
* This may be called without any prior call to {@link EmojiViewCallback#onPressKey(Key)}.
*/
@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());
@ -256,6 +309,20 @@ public final class EmojiPalettesView extends LinearLayout
mKeyboardActionListener.onCodeInput(KeyCode.ALPHA, NOT_A_COORDINATE, NOT_A_COORDINATE, false);
}
@Override
public String getDescription(String emoji) {
if (sDictionaryFacilitator == null) {
return null;
}
var wordProperty = sDictionaryFacilitator.getWordProperty(emoji);
if (wordProperty == null || ! wordProperty.mHasShortcuts) {
return null;
}
return wordProperty.mShortcutTargets.get(0).mWord;
}
public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
if (!enabled) return;
// TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off?
@ -269,11 +336,21 @@ 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();
initDictionaryFacilitator();
}
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) {
@ -295,11 +372,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 +389,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 +409,62 @@ 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 && ! isAnimationsDisabled());
}
if (Settings.getValues().mSecondaryStripVisible) {
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);
private boolean isAnimationsDisabled() {
return android.provider.Settings.Global.getFloat(getContext().getContentResolver(),
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f;
}
public void clearKeyboardCache() {
if (!initialized) {
return;
}
mEmojiCategory.clearKeyboardCache();
mPager.getAdapter().notifyDataSetChanged();
closeDictionaryFacilitator();
}
private void initDictionaryFacilitator() {
if (Settings.getValues().mShowEmojiDescriptions) {
var locale = RichInputMethodManager.getInstance().getCurrentSubtype().getLocale();
if (sDictionaryFacilitator == null || ! sDictionaryFacilitator.isForLocale(locale)) {
closeDictionaryFacilitator();
var dictFile = DictionaryInfoUtils.getCachedDictForLocaleAndType(locale, "emoji", getContext());
var dictionary = dictFile != null? DictionaryFactory.getDictionary(dictFile, locale) : null;
sDictionaryFacilitator = dictionary != null? new SingleDictionaryFacilitator(dictionary) : null;
}
} else {
closeDictionaryFacilitator();
}
}
private static void closeDictionaryFacilitator() {
if (sDictionaryFacilitator != null) {
sDictionaryFacilitator.closeDictionaries();
sDictionaryFacilitator = null;
}
}
}

View file

@ -5,10 +5,10 @@ package helium314.keyboard.keyboard.emoji;
import helium314.keyboard.keyboard.Key;
/**
* Interface to handle touch events from non-View-based elements
* such as Emoji buttons.
* Interface to handle callbacks from child elements
* such as Emoji buttons and keyboard views.
*/
public interface OnKeyEventListener {
public interface EmojiViewCallback {
/**
* Called when a key is pressed by the user
@ -17,8 +17,13 @@ public interface OnKeyEventListener {
/**
* Called when a key is released.
* This may be called without any prior call to {@link OnKeyEventListener#onPressKey(Key)},
* This may be called without any prior call to {@link EmojiViewCallback#onPressKey(Key)},
* for example when a key from a popup keys keyboard is selected by releasing touch on it.
*/
void onReleaseKey(Key key);
/**
* Called from keyboard view to get an emoji description
*/
String getDescription(String emoji);
}

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
val emojiKeyboardHeight = defaultKeyboardHeight * 0.75f + params.mVerticalGap - defaultBottomPadding - context.resources.getDimensionPixelSize(R.dimen.config_emoji_category_page_id_height)
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 = ""

View file

@ -229,15 +229,17 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co
return
// replace comma / period if 2 keys in normal bottom row
if (baseKeys.last().size == 2) {
val newComma = baseKeys.last()[0]
functionalKeysBottom.replaceFirst(
{ it.label == KeyLabel.COMMA || it.groupId == KeyData.GROUP_COMMA},
{ baseKeys.last()[0].copy(newGroupId = 1, newType = baseKeys.last()[0].type ?: it.type) }
{ newComma.copy(newGroupId = 1, newType = newComma.type, newLabelFlags = it.labelFlags or newComma.labelFlags) }
)
val newPeriod = baseKeys.last()[1]
functionalKeysBottom.replaceFirst(
{ it.label == KeyLabel.PERIOD || it.groupId == KeyData.GROUP_PERIOD},
{ baseKeys.last()[1].copy(newGroupId = 2, newType = baseKeys.last()[1].type ?: it.type) }
{ newPeriod.copy(newGroupId = 2, newType = newPeriod.type, newLabelFlags = it.labelFlags or newPeriod.labelFlags) }
)
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 = mutableListOf(Key.POPUP_KEYS_HAS_LABELS)
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 -> tlds.addAll(SpacedTokens(line).map { ".$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
@ -161,6 +156,16 @@ class LocaleKeyboardInfos(dataStream: InputStream?, locale: Locale) {
}
}
fun addLocaleTlds(locale: Locale) {
tlds.add(0, comTld)
val ccLower = locale.country.lowercase()
if (ccLower.isNotEmpty() && locale.language != SubtypeLocaleUtils.NO_LANGUAGE) {
specialCountryTlds[ccLower]?.let { tlds.addAll(SpacedTokens(it)) } ?: tlds.add(".$ccLower")
}
if ((locale.language != "en" && euroLocales.matches(locale.language)) || euroCountries.matches(locale.country))
tlds.add(".eu")
tlds.addAll(SpacedTokens(otherDefaultTlds))
}
}
private fun addFixedColumnOrder(popupKeys: MutableCollection<String>) {
@ -205,12 +210,12 @@ 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)
POPUP_KEYS_ALL -> lkt.addFile(context.assets.open("$LOCALE_TEXTS_FOLDER/more_popups_all.txt"), false)
}
lkt.addLocaleTlds(params.mId.locale)
return lkt
}
@ -220,26 +225,12 @@ private fun getStreamForLocale(locale: Locale, context: Context) =
else context.assets.open("$LOCALE_TEXTS_FOLDER/${locale.toLanguageTag()}.txt")
} catch (_: Exception) {
try {
context.assets.open("$LOCALE_TEXTS_FOLDER/${locale.language}.txt")
context.assets.open("$LOCALE_TEXTS_FOLDER/${if (locale.language == "he") "iw" else locale.language}.txt")
} catch (_: Exception) {
null
}
}
private fun getLocaleTlds(locale: Locale): LinkedHashSet<String> {
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.add(".$ccLower")
return tlds
}
fun clearCache() = localeKeyboardInfosCache.clear()
// cache the texts, so they don't need to be read over and over
@ -263,9 +254,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 +282,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 -> "$"
}
}
@ -332,7 +330,7 @@ const val POPUP_KEYS_NORMAL = "normal"
private const val LOCALE_TEXTS_FOLDER = "locale_key_texts"
// either tld is not simply lowercase ISO 3166-1 code, or there are multiple according to some list
private val specialCountryTlds = listOf(
private val specialCountryTlds = hashMapOf<String, String>(
"bd" to ".bd .com.bd",
"bq" to ".bq .an .nl",
"bl" to ".bl .gp .fr",
@ -342,4 +340,5 @@ private val specialCountryTlds = listOf(
"mf" to ".mf .gp .fr",
"tl" to ".tl .tp",
)
private const val defaultTlds = ".com .gov .edu .org .net"
private const val comTld = ".com"
private const val otherDefaultTlds = ".gov .edu .org .net"

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_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,21 @@ 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
// Intents
const val SEND_INTENT_ONE = -20000
const val SEND_INTENT_TWO = -20001
const val SEND_INTENT_THREE = -20002
/** to make sure a FlorisBoard code works when reading a JSON layout */
fun Int.checkAndConvertCode(): Int = if (this > 0) this else when (this) {
@ -176,13 +189,14 @@ object KeyCode {
REDO, ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT, CLIPBOARD_COPY, CLIPBOARD_PASTE, CLIPBOARD_SELECT_ALL,
CLIPBOARD_SELECT_WORD, TOGGLE_INCOGNITO_MODE, TOGGLE_AUTOCORRECT, MOVE_START_OF_LINE, MOVE_END_OF_LINE,
MOVE_START_OF_PAGE, MOVE_END_OF_PAGE, SHIFT, CAPS_LOCK, MULTIPLE_CODE_POINTS, UNSPECIFIED, CTRL, ALT,
FN, CLIPBOARD_CLEAR_HISTORY, NUMPAD,
FN, CLIPBOARD_CLEAR_HISTORY, NUMPAD, IME_HIDE_UI,
// heliboard only
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, SEND_INTENT_ONE, SEND_INTENT_TWO, SEND_INTENT_THREE,
-> this
// conversion
@ -194,64 +208,19 @@ 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 */
fun Int.toKeyEventCode(): Int = if (this > 0)
when (this.toChar().uppercaseChar()) {
'/' -> KeyEvent.KEYCODE_SLASH
'\\' -> KeyEvent.KEYCODE_BACKSLASH
';' -> KeyEvent.KEYCODE_SEMICOLON
',' -> KeyEvent.KEYCODE_COMMA
'.' -> KeyEvent.KEYCODE_PERIOD
'\'' -> KeyEvent.KEYCODE_APOSTROPHE
'`' -> KeyEvent.KEYCODE_GRAVE
'*' -> KeyEvent.KEYCODE_STAR
']' -> KeyEvent.KEYCODE_RIGHT_BRACKET
'[' -> KeyEvent.KEYCODE_LEFT_BRACKET
'+' -> KeyEvent.KEYCODE_PLUS
'-' -> KeyEvent.KEYCODE_MINUS
'=' -> KeyEvent.KEYCODE_EQUALS
'\n' -> KeyEvent.KEYCODE_ENTER
'\t' -> KeyEvent.KEYCODE_TAB
'0' -> KeyEvent.KEYCODE_0
'1' -> KeyEvent.KEYCODE_1
'2' -> KeyEvent.KEYCODE_2
'3' -> KeyEvent.KEYCODE_3
'4' -> KeyEvent.KEYCODE_4
'5' -> KeyEvent.KEYCODE_5
'6' -> KeyEvent.KEYCODE_6
'7' -> KeyEvent.KEYCODE_7
'8' -> KeyEvent.KEYCODE_8
'9' -> KeyEvent.KEYCODE_9
'A' -> KeyEvent.KEYCODE_A
'B' -> KeyEvent.KEYCODE_B
'C' -> KeyEvent.KEYCODE_C
'D' -> KeyEvent.KEYCODE_D
'E' -> KeyEvent.KEYCODE_E
'F' -> KeyEvent.KEYCODE_F
'G' -> KeyEvent.KEYCODE_G
'H' -> KeyEvent.KEYCODE_H
'I' -> KeyEvent.KEYCODE_I
'J' -> KeyEvent.KEYCODE_J
'K' -> KeyEvent.KEYCODE_K
'L' -> KeyEvent.KEYCODE_L
'M' -> KeyEvent.KEYCODE_M
'N' -> KeyEvent.KEYCODE_N
'O' -> KeyEvent.KEYCODE_O
'P' -> KeyEvent.KEYCODE_P
'Q' -> KeyEvent.KEYCODE_Q
'R' -> KeyEvent.KEYCODE_R
'S' -> KeyEvent.KEYCODE_S
'T' -> KeyEvent.KEYCODE_T
'U' -> KeyEvent.KEYCODE_U
'V' -> KeyEvent.KEYCODE_V
'W' -> KeyEvent.KEYCODE_W
'X' -> KeyEvent.KEYCODE_X
'Y' -> KeyEvent.KEYCODE_Y
'Z' -> KeyEvent.KEYCODE_Z
else -> KeyEvent.KEYCODE_UNKNOWN
}
else when (this) {
fun Int.isModifier() = when (this) {
SHIFT, SYMBOL_ALPHA, ALPHA, SYMBOL, NUMPAD, FN, CTRL, CTRL_LEFT, CTRL_RIGHT, ALT, ALT_LEFT, ALT_RIGHT,
META, META_LEFT, META_RIGHT -> true
else -> false
}
// todo: there are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
/**
* Convert an internal keyCode to a KeyEvent.KEYCODE_<xxx>.
* Positive codes are passed through, unknown negative codes result in KeyEvent.KEYCODE_UNKNOWN.
* To be uses for fake hardware key press.
*/
@JvmStatic fun keyCodeToKeyEventCode(keyCode: Int) = when (keyCode) {
ARROW_UP -> KeyEvent.KEYCODE_DPAD_UP
ARROW_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
ARROW_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
@ -285,6 +254,67 @@ object KeyCode {
F10 -> KeyEvent.KEYCODE_F10
F11 -> KeyEvent.KEYCODE_F11
F12 -> KeyEvent.KEYCODE_F12
else -> if (keyCode < 0) KeyEvent.KEYCODE_UNKNOWN else keyCode
}
// todo: there are many more keys, see near https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_0
/**
* Convert a codePoint to a KeyEvent.KEYCODE_<xxx>.
* Fallback to KeyEvent.KEYCODE_UNKNOWN.
* To be uses for fake hardware key press.
*/
@JvmStatic fun codePointToKeyEventCode(codePoint: Int): Int = when (codePoint.toChar().uppercaseChar()) {
'/' -> KeyEvent.KEYCODE_SLASH
'\\' -> KeyEvent.KEYCODE_BACKSLASH
';' -> KeyEvent.KEYCODE_SEMICOLON
',' -> KeyEvent.KEYCODE_COMMA
'.' -> KeyEvent.KEYCODE_PERIOD
'\'' -> KeyEvent.KEYCODE_APOSTROPHE
'`' -> KeyEvent.KEYCODE_GRAVE
'*' -> KeyEvent.KEYCODE_STAR
']' -> KeyEvent.KEYCODE_RIGHT_BRACKET
'[' -> KeyEvent.KEYCODE_LEFT_BRACKET
'+' -> KeyEvent.KEYCODE_PLUS
'-' -> KeyEvent.KEYCODE_MINUS
'=' -> KeyEvent.KEYCODE_EQUALS
'\n' -> KeyEvent.KEYCODE_ENTER
'\t' -> KeyEvent.KEYCODE_TAB
'0' -> KeyEvent.KEYCODE_0
'1' -> KeyEvent.KEYCODE_1
'2' -> KeyEvent.KEYCODE_2
'3' -> KeyEvent.KEYCODE_3
'4' -> KeyEvent.KEYCODE_4
'5' -> KeyEvent.KEYCODE_5
'6' -> KeyEvent.KEYCODE_6
'7' -> KeyEvent.KEYCODE_7
'8' -> KeyEvent.KEYCODE_8
'9' -> KeyEvent.KEYCODE_9
'A' -> KeyEvent.KEYCODE_A
'B' -> KeyEvent.KEYCODE_B
'C' -> KeyEvent.KEYCODE_C
'D' -> KeyEvent.KEYCODE_D
'E' -> KeyEvent.KEYCODE_E
'F' -> KeyEvent.KEYCODE_F
'G' -> KeyEvent.KEYCODE_G
'H' -> KeyEvent.KEYCODE_H
'I' -> KeyEvent.KEYCODE_I
'J' -> KeyEvent.KEYCODE_J
'K' -> KeyEvent.KEYCODE_K
'L' -> KeyEvent.KEYCODE_L
'M' -> KeyEvent.KEYCODE_M
'N' -> KeyEvent.KEYCODE_N
'O' -> KeyEvent.KEYCODE_O
'P' -> KeyEvent.KEYCODE_P
'Q' -> KeyEvent.KEYCODE_Q
'R' -> KeyEvent.KEYCODE_R
'S' -> KeyEvent.KEYCODE_S
'T' -> KeyEvent.KEYCODE_T
'U' -> KeyEvent.KEYCODE_U
'V' -> KeyEvent.KEYCODE_V
'W' -> KeyEvent.KEYCODE_W
'X' -> KeyEvent.KEYCODE_X
'Y' -> KeyEvent.KEYCODE_Y
'Z' -> KeyEvent.KEYCODE_Z
else -> KeyEvent.KEYCODE_UNKNOWN
}
}

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
@ -17,12 +18,14 @@ import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.settings.SettingsSubtype
import helium314.keyboard.latin.settings.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.latin.settings.createPrefKeyForBooleanSettings
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.DictionaryInfoUtils
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
@ -31,9 +34,8 @@ import helium314.keyboard.latin.utils.ToolbarKey
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
@ -43,14 +45,24 @@ import java.util.EnumMap
class App : Application() {
override fun onCreate() {
super.onCreate()
Settings.init(this)
DebugFlags.init(this)
Settings.init(this)
SubtypeSettings.init(this)
RichInputMethodManager.init(this)
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,81 @@ 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()
}
}
}
if (oldVersion <= 3101) {
val e = prefs.edit()
prefs.all.toMap().forEach { (key, value) ->
if (key == "side_padding_scale") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SIDE_PADDING_SCALE_PREFIX, 0, 2), value as Float)
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SIDE_PADDING_SCALE_PREFIX, 2, 2), value)
} else if (key == "side_padding_scale_landscape") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SIDE_PADDING_SCALE_PREFIX, 1, 2), value as Float)
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SIDE_PADDING_SCALE_PREFIX, 3, 2), value)
} else if (key == "bottom_padding_scale") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_BOTTOM_PADDING_SCALE_PREFIX, 0, 1), value as Float)
} else if (key == "bottom_padding_scale_landscape") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_BOTTOM_PADDING_SCALE_PREFIX, 1, 1), value as Float)
} else if (key == "split_spacer_scale") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SPLIT_SPACER_SCALE_PREFIX, 0, 1), value as Float)
} else if (key == "split_spacer_scale_landscape") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_SPLIT_SPACER_SCALE_PREFIX, 1, 1), value as Float)
} else if (key == "one_handed_mode_enabled_p_true") {
e.putBoolean(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_MODE_PREFIX, 0, 2), value as Boolean)
} else if (key == "one_handed_mode_enabled_p_false") {
e.putBoolean(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_MODE_PREFIX, 1, 2), value as Boolean)
} else if (key == "one_handed_mode_scale_p_true") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_SCALE_PREFIX, 0, 2), value as Float)
} else if (key == "one_handed_mode_scale_p_false") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_SCALE_PREFIX, 1, 2), value as Float)
} else if (key == "one_handed_mode_gravity_p_true") {
e.putInt(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_GRAVITY_PREFIX, 0, 2), value as Int)
} else if (key == "one_handed_mode_gravity_p_false") {
e.putInt(createPrefKeyForBooleanSettings(Settings.PREF_ONE_HANDED_GRAVITY_PREFIX, 1, 2), value as Int)
} else if (key == "keyboard_height_scale") {
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, 1, 1), value as Float)
e.putFloat(createPrefKeyForBooleanSettings(Settings.PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, 1, 1), value)
} else {
if (key == Settings.PREF_ADDITIONAL_SUBTYPES || key == Settings.PREF_ENABLED_SUBTYPES) {
val subtypes = prefs.getString(key, "")!!.split(Separators.SETS).filter { it.isNotEmpty() }.map {
val st = it.toSettingsSubtype()
if (st.locale.language == "ko") st.with(ExtraValue.COMBINING_RULES, "hangul")
else st
}
e.putString(key, subtypes.joinToString(Separators.SETS) { it.toPref() })
} else if (key == Settings.PREF_SELECTED_SUBTYPE) {
val subtype = prefs.getString(key, "")!!.toSettingsSubtype()
if (subtype.locale.language == "ko")
e.putString(key, subtype.with(ExtraValue.COMBINING_RULES, "hangul").toPref())
}
return@forEach
}
e.remove(key)
}
e.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,14 @@
package helium314.keyboard.latin;
import java.util.ArrayList;
import java.util.Locale;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
import helium314.keyboard.latin.makedict.WordProperty;
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 +47,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 +56,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,14 +168,25 @@ 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;
};
}
public WordProperty getWordProperty(final String word, final boolean isBeginningOfSentence) {
return null;
}
/**
* Not a true dictionary. A placeholder used to indicate suggestions that don't come from any
* real dictionary.
*/
static class PhonyDictionary extends Dictionary {
public static class PhonyDictionary extends Dictionary {
PhonyDictionary(final String type) {
super(type, null);
}

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,825 @@
/*
* 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.launch
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 {
try {
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()
} catch (e: Throwable) {
Log.e(TAG, "could not initialize main dictionaries for $locales", e)
}
}
}
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) {
if (word.length <= 1) return
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 {
// adding can throw IllegalArgumentException: Unknown URL content://user_dictionary/words
// https://stackoverflow.com/q/41474623 https://github.com/AnySoftKeyboard/AnySoftKeyboard/issues/490
// apparently some devices don't have a dictionary? or it's just sporadic hiccups?
runCatching { 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.parentFile?.exists() == true || file.parentFile?.mkdirs() == true) file
else null
}
private val blacklist = hashSetOf<String>().apply {
if (blacklistFile?.isFile != 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,102 @@
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)
}
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) {
val dictionary = getDictionary(file, locale) ?: return
if (dicts.any { it.mDictType == dictionary.mDictType }) {
dictionary.close()
return
}
dicts.add(dictionary)
}
@JvmStatic
fun getDictionary(
file: File,
locale: Locale
): Dictionary? {
if (!file.isFile) return null
val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file)
if (header == null) {
killDictionary(file)
return null
}
val dictType = header.mIdString.split(":").first()
val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
file.absolutePath, 0, file.length(), false, locale, dictType
)
if (readOnlyBinaryDictionary.isValidDictionary) {
if (locale.language == "ko") {
// Use KoreanDictionary for Korean locale
return KoreanDictionary(readOnlyBinaryDictionary)
}
return readOnlyBinaryDictionary
}
} else {
readOnlyBinaryDictionary.close()
killDictionary(file)
return null
}
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

@ -23,7 +23,7 @@ import java.util.List;
/**
* A class for detecting Emoji-Alt physical key.
*/
final class EmojiAltPhysicalKeyDetector {
public final class EmojiAltPhysicalKeyDetector {
private static final String TAG = "EmojiAltPhysKeyDetector";
private static final boolean DEBUG = false;

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